From 3a81cea65dc8660c21d76eea3882762a048128bc Mon Sep 17 00:00:00 2001 From: Kristian Bendiksen Date: Fri, 2 Jun 2023 12:22:44 +0200 Subject: [PATCH] #10292 Regression Analysis: add forecasting Fixes #10292. --- .../RimSummaryRegressionAnalysisCurve.cpp | 196 +++++++++++++----- .../RimSummaryRegressionAnalysisCurve.h | 20 ++ .../UnitTests/CMakeLists_files.cmake | 1 + ...RimSummaryRegressionAnalysisCurve-Test.cpp | 64 ++++++ 4 files changed, 230 insertions(+), 51 deletions(-) create mode 100644 ApplicationLibCode/UnitTests/RimSummaryRegressionAnalysisCurve-Test.cpp diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.cpp b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.cpp index 87ce44ac2f..1ac76c3170 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.cpp +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.cpp @@ -18,6 +18,9 @@ #include "RimSummaryRegressionAnalysisCurve.h" +#include "RiaQDateTimeTools.h" +#include "RiaTimeTTools.h" + #include "cafPdmUiLineEditor.h" #include "cafPdmUiTextEditor.h" @@ -28,6 +31,8 @@ #include "PolynominalRegression.hpp" #include "PowerFitRegression.hpp" +#include + #include #include @@ -46,6 +51,16 @@ void caf::AppEnum::setUp() addItem( RimSummaryRegressionAnalysisCurve::RegressionType::LOGISTIC, "LOGISTIC", "Logistic" ); setDefault( RimSummaryRegressionAnalysisCurve::RegressionType::LINEAR ); } + +template <> +void caf::AppEnum::setUp() +{ + addItem( RimSummaryRegressionAnalysisCurve::ForecastUnit::DAYS, "DAYS", "Days" ); + addItem( RimSummaryRegressionAnalysisCurve::ForecastUnit::MONTHS, "MONTHS", "Months" ); + addItem( RimSummaryRegressionAnalysisCurve::ForecastUnit::YEARS, "YEARS", "Years" ); + setDefault( RimSummaryRegressionAnalysisCurve::ForecastUnit::YEARS ); +} + }; // namespace caf //-------------------------------------------------------------------------------------------------- @@ -56,6 +71,9 @@ RimSummaryRegressionAnalysisCurve::RimSummaryRegressionAnalysisCurve() CAF_PDM_InitObject( "Regression Analysis Curve", ":/SummaryCurve16x16.png" ); CAF_PDM_InitFieldNoDefault( &m_regressionType, "RegressionType", "Type" ); + CAF_PDM_InitField( &m_forecastForward, "ForecastForward", 0, "Forward" ); + CAF_PDM_InitField( &m_forecastBackward, "ForecastBackward", 0, "Backward" ); + CAF_PDM_InitFieldNoDefault( &m_forecastUnit, "ForecastUnit", "Unit" ); CAF_PDM_InitField( &m_polynominalDegree, "PolynominalDegree", 3, "Degree" ); CAF_PDM_InitFieldNoDefault( &m_expressionText, "ExpressionText", "Expression" ); @@ -128,87 +146,56 @@ std::tuple, std::vector, QString> { if ( values.empty() || timeSteps.empty() ) return { timeSteps, values, "" }; - auto convertToDouble = []( const std::vector& timeSteps ) - { - std::vector doubleVector( timeSteps.size() ); - std::transform( timeSteps.begin(), - timeSteps.end(), - doubleVector.begin(), - []( const auto& timeVal ) { return static_cast( timeVal ); } ); - return doubleVector; - }; - - auto convertToTimeT = []( const std::vector& timeSteps ) - { - std::vector tVector( timeSteps.size() ); - std::transform( timeSteps.begin(), - timeSteps.end(), - tVector.begin(), - []( const auto& timeVal ) { return static_cast( timeVal ); } ); - return tVector; - }; - - auto filterValues = []( const std::vector& timeSteps, const std::vector& values ) - { - std::vector filteredTimeSteps; - std::vector filteredValues; - for ( size_t i = 0; i < timeSteps.size(); i++ ) - { - if ( timeSteps[i] > 0.0 && values[i] > 0.0 ) - { - filteredTimeSteps.push_back( timeSteps[i] ); - filteredValues.push_back( values[i] ); - } - } - return std::make_pair( filteredTimeSteps, filteredValues ); - }; - std::vector timeStepsD = convertToDouble( timeSteps ); + std::vector outputTimeSteps = getOutputTimeSteps( timeSteps, m_forecastBackward(), m_forecastForward(), m_forecastUnit() ); + + std::vector outputTimeStepsD = convertToDouble( outputTimeSteps ); + if ( m_regressionType == RegressionType::LINEAR ) { regression::LinearRegression linearRegression; linearRegression.fit( timeStepsD, values ); - std::vector predictedValues = linearRegression.predict( timeStepsD ); - return { timeSteps, predictedValues, generateRegressionText( linearRegression ) }; + std::vector predictedValues = linearRegression.predict( outputTimeStepsD ); + return { outputTimeSteps, predictedValues, generateRegressionText( linearRegression ) }; } else if ( m_regressionType == RegressionType::POLYNOMINAL ) { regression::PolynominalRegression polynominalRegression; polynominalRegression.fit( timeStepsD, values, m_polynominalDegree ); - std::vector predictedValues = polynominalRegression.predict( timeStepsD ); - return { timeSteps, predictedValues, generateRegressionText( polynominalRegression ) }; + std::vector predictedValues = polynominalRegression.predict( outputTimeStepsD ); + return { outputTimeSteps, predictedValues, generateRegressionText( polynominalRegression ) }; } else if ( m_regressionType == RegressionType::POWER_FIT ) { - auto [filteredTimeSteps, filteredValues] = filterValues( timeStepsD, values ); + auto [filteredTimeSteps, filteredValues] = getPositiveValues( timeStepsD, values ); regression::PowerFitRegression powerFitRegression; powerFitRegression.fit( filteredTimeSteps, filteredValues ); - std::vector predictedValues = powerFitRegression.predict( filteredTimeSteps ); - return { convertToTimeT( filteredTimeSteps ), predictedValues, generateRegressionText( powerFitRegression ) }; + std::vector predictedValues = powerFitRegression.predict( outputTimeStepsD ); + return { convertToTimeT( outputTimeStepsD ), predictedValues, generateRegressionText( powerFitRegression ) }; } else if ( m_regressionType == RegressionType::EXPONENTIAL ) { - auto [filteredTimeSteps, filteredValues] = filterValues( timeStepsD, values ); + auto [filteredTimeSteps, filteredValues] = getPositiveValues( timeStepsD, values ); regression::ExponentialRegression exponentialRegression; exponentialRegression.fit( filteredTimeSteps, filteredValues ); - std::vector predictedValues = exponentialRegression.predict( filteredTimeSteps ); - return { convertToTimeT( filteredTimeSteps ), predictedValues, generateRegressionText( exponentialRegression ) }; + std::vector predictedValues = exponentialRegression.predict( outputTimeStepsD ); + return { convertToTimeT( outputTimeStepsD ), predictedValues, generateRegressionText( exponentialRegression ) }; } else if ( m_regressionType == RegressionType::LOGARITHMIC ) { - auto [filteredTimeSteps, filteredValues] = filterValues( timeStepsD, values ); + auto [filteredTimeSteps, filteredValues] = getPositiveValues( timeStepsD, values ); regression::LogarithmicRegression logarithmicRegression; logarithmicRegression.fit( filteredTimeSteps, filteredValues ); - std::vector predictedValues = logarithmicRegression.predict( filteredTimeSteps ); - return { convertToTimeT( filteredTimeSteps ), predictedValues, generateRegressionText( logarithmicRegression ) }; + std::vector predictedValues = logarithmicRegression.predict( outputTimeStepsD ); + return { convertToTimeT( outputTimeStepsD ), predictedValues, generateRegressionText( logarithmicRegression ) }; } else if ( m_regressionType == RegressionType::LOGISTIC ) { regression::LogisticRegression logisticRegression; logisticRegression.fit( timeStepsD, values ); - std::vector predictedValues = logisticRegression.predict( timeStepsD ); - return { timeSteps, predictedValues, generateRegressionText( logisticRegression ) }; + std::vector predictedValues = logisticRegression.predict( outputTimeStepsD ); + return { convertToTimeT( outputTimeStepsD ), predictedValues, generateRegressionText( logisticRegression ) }; } return { timeSteps, values, "" }; @@ -231,6 +218,11 @@ void RimSummaryRegressionAnalysisCurve::defineUiOrdering( QString uiConfigName, regressionCurveGroup->add( &m_expressionText ); + caf::PdmUiGroup* forecastingGroup = uiOrdering.addNewGroup( "Forecasting" ); + forecastingGroup->add( &m_forecastForward ); + forecastingGroup->add( &m_forecastBackward ); + forecastingGroup->add( &m_forecastUnit ); + RimSummaryCurve::defineUiOrdering( uiConfigName, uiOrdering ); } @@ -242,7 +234,8 @@ void RimSummaryRegressionAnalysisCurve::fieldChangedByUi( const caf::PdmFieldHan const QVariant& newValue ) { RimSummaryCurve::fieldChangedByUi( changedField, oldValue, newValue ); - if ( changedField == &m_regressionType || changedField == &m_polynominalDegree ) + if ( changedField == &m_regressionType || changedField == &m_polynominalDegree || changedField == &m_forecastBackward || + changedField == &m_forecastForward || changedField == &m_forecastUnit ) { loadAndUpdateDataAndPlot(); } @@ -265,6 +258,14 @@ void RimSummaryRegressionAnalysisCurve::defineEditorAttribute( const caf::PdmFie lineEditorAttr->validator = new QIntValidator( 1, 50, nullptr ); } } + else if ( field == &m_forecastForward || field == &m_forecastBackward ) + { + if ( auto* lineEditorAttr = dynamic_cast( attribute ) ) + { + // Block negative forecast + lineEditorAttr->validator = new QIntValidator( 0, 50, nullptr ); + } + } else if ( field == &m_expressionText ) { auto myAttr = dynamic_cast( attribute ); @@ -382,3 +383,96 @@ QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regress // TODO: Display more parameters here. return ""; } + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +void RimSummaryRegressionAnalysisCurve::appendTimeSteps( std::vector& destinationTimeSteps, const std::set& sourceTimeSteps ) +{ + for ( const QDateTime& t : sourceTimeSteps ) + destinationTimeSteps.push_back( RiaTimeTTools::fromQDateTime( t ) ); +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimSummaryRegressionAnalysisCurve::getOutputTimeSteps( const std::vector& timeSteps, + int forecastBackward, + int forecastForward, + ForecastUnit forecastUnit ) +{ + auto getTimeSpan = []( int value, ForecastUnit unit ) + { + if ( unit == ForecastUnit::YEARS ) return DateTimeSpan( value, 0, 0 ); + if ( unit == ForecastUnit::MONTHS ) return DateTimeSpan( 0, value, 0 ); + CAF_ASSERT( unit == ForecastUnit::DAYS ); + return DateTimeSpan( 0, 0, value ); + }; + + int numDates = 50; + + std::vector outputTimeSteps; + if ( forecastBackward > 0 ) + { + QDateTime firstTimeStepInData = RiaQDateTimeTools::fromTime_t( timeSteps.front() ); + QDateTime forecastStartTimeStep = RiaQDateTimeTools::subtractSpan( firstTimeStepInData, getTimeSpan( forecastBackward, forecastUnit ) ); + auto forecastTimeSteps = + RiaQDateTimeTools::createEvenlyDistributedDatesInInterval( forecastStartTimeStep, firstTimeStepInData, numDates ); + appendTimeSteps( outputTimeSteps, forecastTimeSteps ); + } + + outputTimeSteps.insert( std::end( outputTimeSteps ), std::begin( timeSteps ), std::end( timeSteps ) ); + + if ( forecastForward > 0 ) + { + QDateTime lastTimeStepInData = RiaQDateTimeTools::fromTime_t( timeSteps.back() ); + QDateTime forecastEndTimeStep = RiaQDateTimeTools::addSpan( lastTimeStepInData, getTimeSpan( forecastForward, forecastUnit ) ); + auto forecastTimeSteps = RiaQDateTimeTools::createEvenlyDistributedDatesInInterval( lastTimeStepInData, forecastEndTimeStep, numDates ); + appendTimeSteps( outputTimeSteps, forecastTimeSteps ); + } + + return outputTimeSteps; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimSummaryRegressionAnalysisCurve::convertToDouble( const std::vector& timeSteps ) +{ + std::vector doubleVector( timeSteps.size() ); + std::transform( timeSteps.begin(), + timeSteps.end(), + doubleVector.begin(), + []( const auto& timeVal ) { return static_cast( timeVal ); } ); + return doubleVector; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::vector RimSummaryRegressionAnalysisCurve::convertToTimeT( const std::vector& timeSteps ) +{ + std::vector tVector( timeSteps.size() ); + std::transform( timeSteps.begin(), timeSteps.end(), tVector.begin(), []( const auto& timeVal ) { return static_cast( timeVal ); } ); + return tVector; +} + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +std::pair, std::vector> + RimSummaryRegressionAnalysisCurve::getPositiveValues( const std::vector& timeSteps, const std::vector& values ) +{ + std::vector filteredTimeSteps; + std::vector filteredValues; + for ( size_t i = 0; i < timeSteps.size(); i++ ) + { + if ( timeSteps[i] > 0.0 && values[i] > 0.0 ) + { + filteredTimeSteps.push_back( timeSteps[i] ); + filteredValues.push_back( values[i] ); + } + } + + return std::make_pair( filteredTimeSteps, filteredValues ); +} diff --git a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.h b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.h index 75f08a52c7..64982c8e2b 100644 --- a/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.h +++ b/ApplicationLibCode/ProjectDataModel/Summary/RimSummaryRegressionAnalysisCurve.h @@ -57,6 +57,13 @@ public: LOGISTIC }; + enum class ForecastUnit + { + DAYS, + MONTHS, + YEARS, + }; + RimSummaryRegressionAnalysisCurve(); ~RimSummaryRegressionAnalysisCurve() override; @@ -67,6 +74,8 @@ public: // X Axis functions std::vector valuesX() const override; std::vector timeStepsX() const override; + static std::vector + getOutputTimeSteps( const std::vector& timeSteps, int forecastBackward, int forecastForward, ForecastUnit forecastUnit ); private: void onLoadDataAndUpdate( bool updateParentPlot ) override; @@ -82,6 +91,12 @@ private: std::tuple, std::vector, QString> computeRegressionCurve( const std::vector& timeSteps, const std::vector& values ) const; + static std::vector convertToDouble( const std::vector& timeSteps ); + static std::vector convertToTimeT( const std::vector& timeSteps ); + + static std::pair, std::vector> getPositiveValues( const std::vector& timeSteps, + const std::vector& values ); + static QString generateRegressionText( const regression::LinearRegression& reg ); static QString generateRegressionText( const regression::PolynominalRegression& reg ); static QString generateRegressionText( const regression::PowerFitRegression& reg ); @@ -91,9 +106,14 @@ private: static QString formatDouble( double v ); + static void appendTimeSteps( std::vector& destinationTimeSteps, const std::set& sourceTimeSteps ); + caf::PdmField> m_regressionType; caf::PdmField m_polynominalDegree; caf::PdmField m_expressionText; + caf::PdmField m_forecastForward; + caf::PdmField m_forecastBackward; + caf::PdmField> m_forecastUnit; std::vector m_valuesX; std::vector m_timeStepsX; diff --git a/ApplicationLibCode/UnitTests/CMakeLists_files.cmake b/ApplicationLibCode/UnitTests/CMakeLists_files.cmake index 07e51f9c2d..691c226c26 100644 --- a/ApplicationLibCode/UnitTests/CMakeLists_files.cmake +++ b/ApplicationLibCode/UnitTests/CMakeLists_files.cmake @@ -94,6 +94,7 @@ set(SOURCE_GROUP_SOURCE_FILES ${CMAKE_CURRENT_LIST_DIR}/RigWellLogCurveData-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RimWellLogCalculatedCurve-Test.cpp ${CMAKE_CURRENT_LIST_DIR}/RifReaderFmuRft-Test.cpp + ${CMAKE_CURRENT_LIST_DIR}/RimSummaryRegressionAnalysisCurve-Test.cpp ) if(RESINSIGHT_ENABLE_GRPC) diff --git a/ApplicationLibCode/UnitTests/RimSummaryRegressionAnalysisCurve-Test.cpp b/ApplicationLibCode/UnitTests/RimSummaryRegressionAnalysisCurve-Test.cpp new file mode 100644 index 0000000000..cc582280a5 --- /dev/null +++ b/ApplicationLibCode/UnitTests/RimSummaryRegressionAnalysisCurve-Test.cpp @@ -0,0 +1,64 @@ +///////////////////////////////////////////////////////////////////////////////// +// +// Copyright (C) 2023- 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 +// for more details. +// + +#include "gtest/gtest.h" + +#include "RiaQDateTimeTools.h" +#include "RiaTimeTTools.h" +#include "RimSummaryRegressionAnalysisCurve.h" + +#include + +//-------------------------------------------------------------------------------------------------- +/// +//-------------------------------------------------------------------------------------------------- +TEST( RimSummaryRegressionAnalysisCurve, getOutputTimeStepsNoForecast ) +{ + const std::vector timeSteps = { 100000 }; + const std::vector output = + RimSummaryRegressionAnalysisCurve::getOutputTimeSteps( timeSteps, 0, 0, RimSummaryRegressionAnalysisCurve::ForecastUnit::MONTHS ); + + ASSERT_EQ( timeSteps, output ); +} + +TEST( RimSummaryRegressionAnalysisCurve, getOutputTimeStepsForwardForecast ) +{ + QDateTime dt = RiaQDateTimeTools::fromYears( 2020 ); + const std::vector timeSteps = { RiaTimeTTools::fromQDateTime( dt ) }; + + int forecastBackward = 0; + int forecastForward = 1; + const std::vector output = + RimSummaryRegressionAnalysisCurve::getOutputTimeSteps( timeSteps, + forecastBackward, + forecastForward, + RimSummaryRegressionAnalysisCurve::ForecastUnit::YEARS ); + + ASSERT_EQ( output.size(), 51u ); + + // First output value should be the original value in time steps + ASSERT_EQ( timeSteps[0], output[0] ); + + QDateTime oneYearLater = dt.addYears( 1 ); + for ( size_t i = 1; i < output.size(); i++ ) + { + auto d = RiaQDateTimeTools::fromTime_t( output[i] ); + ASSERT_FALSE( RiaQDateTimeTools::lessThan( d, dt ) ); + ASSERT_TRUE( RiaQDateTimeTools::lessThan( d, oneYearLater ) ); + } + // +}