diff --git a/ApplicationLibCode/Application/Tools/RiaCurveMerger.inl b/ApplicationLibCode/Application/Tools/RiaCurveMerger.inl index 87b21a9589..4110a16e7e 100644 --- a/ApplicationLibCode/Application/Tools/RiaCurveMerger.inl +++ b/ApplicationLibCode/Application/Tools/RiaCurveMerger.inl @@ -69,23 +69,29 @@ RiaCurveMerger::RiaCurveMerger( RiaCurveDefines::InterpolationMethod template void RiaCurveMerger::addCurveData( const std::vector& xValues, const std::vector& yValues ) { - CAF_ASSERT( xValues.size() == yValues.size() ); + // X and Y can have different sizes when the data is read from file. This is a legitimate run-time condition, e.g. for an + // ongoing simulation where the last report step is not complete, or when depth values and result values come from different + // sources. computeInterpolatedValues() assumes that the two vectors have the same number of samples, so discard the trailing + // samples that have no counterpart. See https://github.com/OPM/ResInsight/issues/12810 + const size_t sampleCount = std::min( xValues.size(), yValues.size() ); + if ( sampleCount == 0 ) return; - if ( !xValues.empty() ) + std::vector x( xValues.begin(), xValues.begin() + sampleCount ); + std::vector y( yValues.begin(), yValues.begin() + sampleCount ); + + if ( m_originalValues.empty() ) { - if ( m_originalValues.empty() ) - { - m_isXValuesSharedBetweenCurves = true; - m_isXValuesMonotonicallyIncreasing = isMonotonicallyIncreasing( xValues ); - } - else - { - const auto& firstXValues = m_originalValues.front().first; - m_isXValuesSharedBetweenCurves &= std::equal( firstXValues.begin(), firstXValues.end(), xValues.begin(), xValues.end() ); - m_isXValuesMonotonicallyIncreasing &= isMonotonicallyIncreasing( xValues ); - } - m_originalValues.push_back( std::make_pair( xValues, yValues ) ); + m_isXValuesSharedBetweenCurves = true; + m_isXValuesMonotonicallyIncreasing = isMonotonicallyIncreasing( x ); } + else + { + const auto& firstXValues = m_originalValues.front().first; + m_isXValuesSharedBetweenCurves &= std::equal( firstXValues.begin(), firstXValues.end(), x.begin(), x.end() ); + m_isXValuesMonotonicallyIncreasing &= isMonotonicallyIncreasing( x ); + } + + m_originalValues.push_back( std::make_pair( std::move( x ), std::move( y ) ) ); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/Application/Tools/RiaWellLogCurveMerger.cpp b/ApplicationLibCode/Application/Tools/RiaWellLogCurveMerger.cpp index 8cf2d352a1..fb221d99d4 100644 --- a/ApplicationLibCode/Application/Tools/RiaWellLogCurveMerger.cpp +++ b/ApplicationLibCode/Application/Tools/RiaWellLogCurveMerger.cpp @@ -37,12 +37,14 @@ RiaWellLogCurveMerger::RiaWellLogCurveMerger() //-------------------------------------------------------------------------------------------------- void RiaWellLogCurveMerger::addCurveData( const std::vector& xValues, const std::vector& yValues ) { - CAF_ASSERT( xValues.size() == yValues.size() ); + // Mismatching sizes is a legitimate run-time condition for data read from file. lookupYValue() discards the whole curve when + // the sizes differ, so truncate to the common sample count to keep the samples that do have a counterpart. + // See https://github.com/OPM/ResInsight/issues/12810 + const size_t sampleCount = std::min( xValues.size(), yValues.size() ); + if ( sampleCount == 0 ) return; - if ( !xValues.empty() ) - { - m_originalValues.push_back( std::make_pair( xValues, yValues ) ); - } + m_originalValues.push_back( std::make_pair( std::vector( xValues.begin(), xValues.begin() + sampleCount ), + std::vector( yValues.begin(), yValues.begin() + sampleCount ) ) ); } //-------------------------------------------------------------------------------------------------- diff --git a/ApplicationLibCode/UnitTests/RigTimeCurveHistoryMerger-Test.cpp b/ApplicationLibCode/UnitTests/RigTimeCurveHistoryMerger-Test.cpp index bc845fe409..e92f9f60f0 100644 --- a/ApplicationLibCode/UnitTests/RigTimeCurveHistoryMerger-Test.cpp +++ b/ApplicationLibCode/UnitTests/RigTimeCurveHistoryMerger-Test.cpp @@ -234,3 +234,56 @@ TEST( RiaTimeHistoryCurveMergerTest, SharedXValues ) 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 timeSteps{ 1, 2, 3, 4, 5, 6, 7 }; + std::vector 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 timeSteps{ 1, 2, 3, 4, 5, 6, 7 }; + std::vector completeValues{ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0 }; + std::vector 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] ); + } +}