Files
ResInsight/ApplicationLibCode/UnitTests/RigTimeCurveHistoryMerger-Test.cpp
T
Magne Sjaastad aa01d5e4dc #12810 Guard curve mergers against mismatching X and Y sample counts
RiaCurveMerger::addCurveData and RiaWellLogCurveMerger::addCurveData only
validated that the X and Y vectors have the same size with CAF_ASSERT, which is
compiled out in optimized builds. Mismatching sizes are a legitimate run-time
condition for data read from file, so the check has to hold in release builds as
well.

In RiaCurveMerger the shared-X fast path in computeInterpolatedValues() indexes
the Y vector of every curve using the sample count of the first curve. A curve
with identical X values but fewer Y values therefore read past the end of its
heap buffer inside the OpenMP loop. RimEnsembleStatisticsCase passes time steps
and values straight from the summary reader without reconciling the sizes, and
realizations in an ensemble normally share time steps, so this was reachable for
an ensemble containing an ongoing simulation.

Both mergers now truncate the incoming data to the common sample count instead.
For RiaWellLogCurveMerger this is also an improvement in behaviour, since
lookupYValue() used to discard the whole curve when the sizes differed.

Add unit tests covering a single curve with fewer values than time steps and
curves with shared time steps where one curve has fewer values.
2026-08-10 09:41:56 +02:00

290 lines
12 KiB
C++

#include "gtest/gtest.h"
#include "RiaCurveMerger.h"
#include <cmath> // Needed for HUGE_VAL on Linux
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaTimeHistoryCurveMergerTest, TestDateInterpolation )
{
std::vector<double> values{ 2.0, 3.5, 5.0, 6.0 };
std::vector<time_t> timeSteps{ 1, 5, 10, 15 };
auto interpolationMethod = RiaCurveDefines::InterpolationMethod::LINEAR;
{
double val = RiaTimeHistoryCurveMerger::interpolatedYValue( 1, timeSteps, values, interpolationMethod );
EXPECT_EQ( 2.0, val );
}
{
double val = RiaTimeHistoryCurveMerger::interpolatedYValue( 0, timeSteps, values, interpolationMethod );
EXPECT_EQ( HUGE_VAL, val );
}
{
double val = RiaTimeHistoryCurveMerger::interpolatedYValue( 20, timeSteps, values, interpolationMethod );
EXPECT_EQ( HUGE_VAL, val );
}
{
double val = RiaTimeHistoryCurveMerger::interpolatedYValue( 3, timeSteps, values, interpolationMethod );
EXPECT_EQ( 2.75, val );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaTimeHistoryCurveMergerTest, ExtractIntervalsWithSameTimeSteps )
{
std::vector<double> valuesA{ HUGE_VAL, 1.0, HUGE_VAL, 2.0, 2.5, 3.0, 4.0, 5.0, 6.0, HUGE_VAL };
std::vector<double> valuesB{ 10, 20, 30, 40, 45, HUGE_VAL, HUGE_VAL, 5.0, 6.0, HUGE_VAL };
EXPECT_EQ( valuesA.size(), valuesB.size() );
std::vector<time_t> timeSteps;
for ( size_t i = 0; i < 10; i++ )
{
timeSteps.push_back( i );
}
RiaTimeHistoryCurveMerger interpolate( RiaCurveDefines::InterpolationMethod::LINEAR );
interpolate.addCurveData( timeSteps, valuesA );
interpolate.addCurveData( timeSteps, valuesB );
interpolate.computeInterpolatedValues( true );
auto interpolatedTimeSteps = interpolate.allXValues();
auto intervals = interpolate.validIntervalsForAllXValues();
EXPECT_EQ( 10, static_cast<int>( interpolatedTimeSteps.size() ) );
EXPECT_EQ( 3, static_cast<int>( intervals.size() ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaTimeHistoryCurveMergerTest, ExtractIntervalsWithSameTimeStepsOneComplete )
{
std::vector<double> valuesA{ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 };
std::vector<double> valuesB{ 10, 20, 30, HUGE_VAL, 50, HUGE_VAL, 70 };
EXPECT_EQ( valuesA.size(), valuesB.size() );
std::vector<time_t> timeSteps;
for ( size_t i = 0; i < 7; i++ )
{
timeSteps.push_back( i );
}
RiaTimeHistoryCurveMerger interpolate( RiaCurveDefines::InterpolationMethod::LINEAR );
interpolate.addCurveData( timeSteps, valuesA );
interpolate.addCurveData( timeSteps, valuesB );
interpolate.computeInterpolatedValues( true );
auto interpolatedTimeSteps = interpolate.allXValues();
auto intervals = interpolate.validIntervalsForAllXValues();
EXPECT_EQ( 7, static_cast<int>( interpolatedTimeSteps.size() ) );
EXPECT_EQ( 3, static_cast<int>( intervals.size() ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaTimeHistoryCurveMergerTest, ExtractIntervalsWithSameTimeStepsBothComplete )
{
std::vector<double> valuesA{ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 };
std::vector<double> valuesB{ 10, 20, 30, 40, 50, 60, 70 };
EXPECT_EQ( valuesA.size(), valuesB.size() );
std::vector<time_t> timeSteps;
for ( size_t i = 0; i < 7; i++ )
{
timeSteps.push_back( i );
}
RiaTimeHistoryCurveMerger interpolate( RiaCurveDefines::InterpolationMethod::LINEAR );
interpolate.addCurveData( timeSteps, valuesA );
interpolate.addCurveData( timeSteps, valuesB );
interpolate.computeInterpolatedValues( true );
auto interpolatedTimeSteps = interpolate.allXValues();
auto intervals = interpolate.validIntervalsForAllXValues();
EXPECT_EQ( 7, static_cast<int>( interpolatedTimeSteps.size() ) );
EXPECT_EQ( 1, static_cast<int>( intervals.size() ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaTimeHistoryCurveMergerTest, OverlappintTimes )
{
std::vector<double> valuesA{ 1, 2, 3, 4, 5 };
std::vector<double> valuesB{ 10, 20, 30, 40, 50 };
EXPECT_EQ( valuesA.size(), valuesB.size() );
std::vector<time_t> timeStepsA{ 0, 10, 11, 15, 20 };
std::vector<time_t> timeStepsB{ 1, 2, 3, 5, 7 };
RiaTimeHistoryCurveMerger interpolate( RiaCurveDefines::InterpolationMethod::LINEAR );
interpolate.addCurveData( timeStepsA, valuesA );
interpolate.addCurveData( timeStepsB, valuesB );
interpolate.computeInterpolatedValues( true );
EXPECT_EQ( 2, static_cast<int>( interpolate.curveCount() ) );
auto interpolatedTimeSteps = interpolate.allXValues();
auto intervals = interpolate.validIntervalsForAllXValues();
EXPECT_EQ( 10, static_cast<int>( interpolatedTimeSteps.size() ) );
EXPECT_EQ( 1, static_cast<int>( intervals.size() ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaTimeHistoryCurveMergerTest, RobustUse )
{
{
RiaTimeHistoryCurveMerger curveMerger( RiaCurveDefines::InterpolationMethod::LINEAR );
curveMerger.computeInterpolatedValues( true );
EXPECT_EQ( 0, static_cast<int>( curveMerger.allXValues().size() ) );
}
std::vector<double> valuesA{ 1, 2, 3, 4, 5 };
std::vector<double> valuesB{ 10, 20, 30 };
std::vector<time_t> timeStepsA{ 0, 10, 11, 15, 20 };
std::vector<time_t> timeStepsB{ 1, 2, 3 };
{
RiaTimeHistoryCurveMerger curveMerger( RiaCurveDefines::InterpolationMethod::LINEAR );
curveMerger.addCurveData( timeStepsA, valuesA );
curveMerger.computeInterpolatedValues( true );
EXPECT_EQ( timeStepsA.size(), curveMerger.allXValues().size() );
EXPECT_EQ( timeStepsA.size(), curveMerger.interpolatedYValuesForAllXValues( 0 ).size() );
}
{
RiaTimeHistoryCurveMerger curveMerger( RiaCurveDefines::InterpolationMethod::LINEAR );
curveMerger.addCurveData( timeStepsA, valuesA );
curveMerger.addCurveData( timeStepsB, valuesB );
// Execute interpolation twice is allowed
curveMerger.computeInterpolatedValues( true );
curveMerger.computeInterpolatedValues( true );
EXPECT_EQ( 8, static_cast<int>( curveMerger.allXValues().size() ) );
EXPECT_EQ( 8, static_cast<int>( curveMerger.interpolatedYValuesForAllXValues( 0 ).size() ) );
EXPECT_EQ( 8, static_cast<int>( curveMerger.interpolatedYValuesForAllXValues( 1 ).size() ) );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaTimeHistoryCurveMergerTest, NoTimeStepOverlap )
{
std::vector<double> valuesA{ 1, 2, 3, 4, 5 };
std::vector<double> valuesB{ 10, 20, 30 };
std::vector<time_t> timeStepsA{ 0, 10, 11, 15, 20 };
std::vector<time_t> timeStepsB{ 100, 200, 300 };
{
RiaTimeHistoryCurveMerger curveMerger( RiaCurveDefines::InterpolationMethod::LINEAR );
curveMerger.addCurveData( timeStepsA, valuesA );
curveMerger.addCurveData( timeStepsB, valuesB );
// Execute interpolation twice is allowed
curveMerger.computeInterpolatedValues( true );
EXPECT_EQ( 8, static_cast<int>( curveMerger.allXValues().size() ) );
EXPECT_EQ( 0, static_cast<int>( curveMerger.validIntervalsForAllXValues().size() ) );
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RiaTimeHistoryCurveMergerTest, SharedXValues )
{
std::vector<double> valuesA{ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 };
std::vector<double> valuesB{ 10, 20, 30, 40, 50, 60, 70 };
std::vector<time_t> timeSteps{ 1, 2, 3, 4, 5, 6, 7 };
RiaTimeHistoryCurveMerger interpolate( RiaCurveDefines::InterpolationMethod::LINEAR );
interpolate.addCurveData( timeSteps, valuesA );
interpolate.addCurveData( timeSteps, valuesB );
interpolate.computeInterpolatedValues( true );
auto interpolatedTimeSteps = interpolate.allXValues();
EXPECT_TRUE( std::equal( timeSteps.begin(), timeSteps.end(), interpolatedTimeSteps.begin() ) );
auto generatedYValuesA = interpolate.interpolatedYValuesForAllXValues( 0 );
EXPECT_TRUE( std::equal( valuesA.begin(), valuesA.end(), generatedYValuesA.begin() ) );
auto generatedYValuesB = interpolate.interpolatedYValuesForAllXValues( 1 );
EXPECT_TRUE( std::equal( valuesB.begin(), valuesB.end(), generatedYValuesB.begin() ) );
}
//--------------------------------------------------------------------------------------------------
/// A curve where the value count is smaller than the time step count must not read out of bounds.
/// See https://github.com/OPM/ResInsight/issues/12810
//--------------------------------------------------------------------------------------------------
TEST( RiaTimeHistoryCurveMergerTest, FewerValuesThanTimeSteps )
{
std::vector<time_t> timeSteps{ 1, 2, 3, 4, 5, 6, 7 };
std::vector<double> partialValues{ 1.0, 2.0, 3.0, 4.0 };
RiaTimeHistoryCurveMerger interpolate( RiaCurveDefines::InterpolationMethod::LINEAR );
interpolate.addCurveData( timeSteps, partialValues );
interpolate.computeInterpolatedValues( true );
// The trailing time steps without a value are discarded
EXPECT_EQ( partialValues.size(), interpolate.allXValues().size() );
auto generatedValues = interpolate.interpolatedYValuesForAllXValues( 0 );
ASSERT_EQ( partialValues.size(), generatedValues.size() );
EXPECT_TRUE( std::equal( partialValues.begin(), partialValues.end(), generatedValues.begin() ) );
}
//--------------------------------------------------------------------------------------------------
/// Curves sharing time steps, but where one curve has fewer values, must not read out of bounds in
/// the shared-X code path. See https://github.com/OPM/ResInsight/issues/12810
//--------------------------------------------------------------------------------------------------
TEST( RiaTimeHistoryCurveMergerTest, SharedXValuesWithFewerValuesInOneCurve )
{
std::vector<time_t> timeSteps{ 1, 2, 3, 4, 5, 6, 7 };
std::vector<double> completeValues{ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 };
std::vector<double> partialValues{ 10.0, 20.0, 30.0, 40.0 };
RiaTimeHistoryCurveMerger interpolate( RiaCurveDefines::InterpolationMethod::LINEAR );
interpolate.addCurveData( timeSteps, completeValues );
interpolate.addCurveData( timeSteps, partialValues );
interpolate.computeInterpolatedValues( true );
EXPECT_EQ( timeSteps.size(), interpolate.allXValues().size() );
auto generatedComplete = interpolate.interpolatedYValuesForAllXValues( 0 );
ASSERT_EQ( timeSteps.size(), generatedComplete.size() );
EXPECT_TRUE( std::equal( completeValues.begin(), completeValues.end(), generatedComplete.begin() ) );
auto generatedPartial = interpolate.interpolatedYValuesForAllXValues( 1 );
ASSERT_EQ( timeSteps.size(), generatedPartial.size() );
EXPECT_TRUE( std::equal( partialValues.begin(), partialValues.end(), generatedPartial.begin() ) );
// No data is available for the time steps beyond the last value
for ( size_t i = partialValues.size(); i < generatedPartial.size(); i++ )
{
EXPECT_EQ( HUGE_VAL, generatedPartial[i] );
}
}