///////////////////////////////////////////////////////////////////////////////// // // 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 "RimSummaryRegressionAnalysisCurve.h" #include "RiaQDateTimeTools.h" #include "RiaTimeTTools.h" #include "RimSummaryPlot.h" #include "RimTimeAxisAnnotation.h" #include "cafPdmUiDateEditor.h" #include "cafPdmUiLineEditor.h" #include "cafPdmUiSliderEditor.h" #include "cafPdmUiTextEditor.h" #include "ExponentialRegression.hpp" #include "LinearRegression.hpp" #include "LogarithmicRegression.hpp" #include "LogisticRegression.hpp" #include "PolynominalRegression.hpp" #include "PowerFitRegression.hpp" #include #include #include CAF_PDM_SOURCE_INIT( RimSummaryRegressionAnalysisCurve, "RegressionAnalysisCurve" ); namespace caf { template <> void caf::AppEnum::setUp() { addItem( RimSummaryRegressionAnalysisCurve::RegressionType::LINEAR, "LINEAR", "Linear" ); addItem( RimSummaryRegressionAnalysisCurve::RegressionType::POLYNOMINAL, "POLYNOMINAL", "Polynominal" ); addItem( RimSummaryRegressionAnalysisCurve::RegressionType::POWER_FIT, "POWER_FIT", "Power Fit" ); addItem( RimSummaryRegressionAnalysisCurve::RegressionType::EXPONENTIAL, "EXPONENTIAL", "Exponential" ); addItem( RimSummaryRegressionAnalysisCurve::RegressionType::LOGARITHMIC, "LOGARITHMIC", "Logarithmic" ); 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 //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- 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_minTimeStep, "MinTimeStep", "From" ); m_minTimeStep.uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() ); CAF_PDM_InitFieldNoDefault( &m_maxTimeStep, "MaxTimeStep", "To" ); m_maxTimeStep.uiCapability()->setUiEditorTypeName( caf::PdmUiSliderEditor::uiEditorTypeName() ); CAF_PDM_InitField( &m_showTimeSelectionInPlot, "ShowTimeSelectionInPlot", false, "Show In Plot" ); CAF_PDM_InitFieldNoDefault( &m_expressionText, "ExpressionText", "Expression" ); m_expressionText.uiCapability()->setUiEditorTypeName( caf::PdmUiTextEditor::uiEditorTypeName() ); m_expressionText.uiCapability()->setUiLabelPosition( caf::PdmUiItemInfo::HIDDEN ); m_expressionText.uiCapability()->setUiReadOnly( true ); m_expressionText.xmlCapability()->disableIO(); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- RimSummaryRegressionAnalysisCurve::~RimSummaryRegressionAnalysisCurve() { auto plot = firstAncestorOrThisOfType(); if ( plot && m_timeRangeAnnotation ) plot->removeTimeAnnotation( m_timeRangeAnnotation ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSummaryRegressionAnalysisCurve::onLoadDataAndUpdate( bool updateParentPlot ) { QString descriptionX; std::tie( m_timeStepsX, m_valuesX, descriptionX ) = computeRegressionCurve( RimSummaryCurve::timeStepsX(), RimSummaryCurve::valuesX() ); QString descriptionY; std::tie( m_timeStepsY, m_valuesY, descriptionY ) = computeRegressionCurve( RimSummaryCurve::timeStepsY(), RimSummaryCurve::valuesY() ); m_expressionText = descriptionY; RimSummaryCurve::onLoadDataAndUpdate( updateParentPlot ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimSummaryRegressionAnalysisCurve::valuesY() const { return m_valuesY; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimSummaryRegressionAnalysisCurve::valuesX() const { return m_valuesX; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimSummaryRegressionAnalysisCurve::timeStepsY() const { return m_timeStepsY; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::vector RimSummaryRegressionAnalysisCurve::timeStepsX() const { return m_timeStepsX; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::tuple, std::vector, QString> RimSummaryRegressionAnalysisCurve::computeRegressionCurve( const std::vector& timeSteps, const std::vector& values ) const { if ( values.empty() || timeSteps.empty() ) return { timeSteps, values, "" }; auto [timeStepsInRange, valuesInRange] = getInRangeValues( timeSteps, values, m_minTimeStep, m_maxTimeStep ); if ( timeStepsInRange.empty() || valuesInRange.empty() ) return {}; std::vector timeStepsD = convertToDouble( timeStepsInRange ); std::vector outputTimeSteps = getOutputTimeSteps( timeStepsInRange, m_forecastBackward(), m_forecastForward(), m_forecastUnit() ); std::vector outputTimeStepsD = convertToDouble( outputTimeSteps ); if ( timeStepsD.empty() || valuesInRange.empty() ) { return {}; } if ( m_regressionType == RegressionType::LINEAR ) { regression::LinearRegression linearRegression; linearRegression.fit( timeStepsD, valuesInRange ); std::vector predictedValues = linearRegression.predict( outputTimeStepsD ); return { outputTimeSteps, predictedValues, generateRegressionText( linearRegression ) }; } else if ( m_regressionType == RegressionType::POLYNOMINAL ) { regression::PolynominalRegression polynominalRegression; polynominalRegression.fit( timeStepsD, valuesInRange, m_polynominalDegree ); std::vector predictedValues = polynominalRegression.predict( outputTimeStepsD ); return { outputTimeSteps, predictedValues, generateRegressionText( polynominalRegression ) }; } else if ( m_regressionType == RegressionType::POWER_FIT ) { auto [filteredTimeSteps, filteredValues] = getPositiveValues( timeStepsD, valuesInRange ); regression::PowerFitRegression powerFitRegression; powerFitRegression.fit( filteredTimeSteps, filteredValues ); std::vector predictedValues = powerFitRegression.predict( outputTimeStepsD ); return { convertToTimeT( outputTimeStepsD ), predictedValues, generateRegressionText( powerFitRegression ) }; } else if ( m_regressionType == RegressionType::EXPONENTIAL ) { auto [filteredTimeSteps, filteredValues] = getPositiveValues( timeStepsD, valuesInRange ); regression::ExponentialRegression exponentialRegression; exponentialRegression.fit( filteredTimeSteps, filteredValues ); std::vector predictedValues = exponentialRegression.predict( outputTimeStepsD ); return { convertToTimeT( outputTimeStepsD ), predictedValues, generateRegressionText( exponentialRegression ) }; } else if ( m_regressionType == RegressionType::LOGARITHMIC ) { auto [filteredTimeSteps, filteredValues] = getPositiveValues( timeStepsD, valuesInRange ); regression::LogarithmicRegression logarithmicRegression; logarithmicRegression.fit( filteredTimeSteps, filteredValues ); std::vector predictedValues = logarithmicRegression.predict( outputTimeStepsD ); return { convertToTimeT( outputTimeStepsD ), predictedValues, generateRegressionText( logarithmicRegression ) }; } else if ( m_regressionType == RegressionType::LOGISTIC ) { regression::LogisticRegression logisticRegression; logisticRegression.fit( timeStepsD, valuesInRange ); std::vector predictedValues = logisticRegression.predict( outputTimeStepsD ); return { convertToTimeT( outputTimeStepsD ), predictedValues, generateRegressionText( logisticRegression ) }; } return { timeSteps, values, "" }; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSummaryRegressionAnalysisCurve::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) { RimPlotCurve::updateFieldUiState(); caf::PdmUiGroup* regressionCurveGroup = uiOrdering.addNewGroup( "Regression Analysis" ); regressionCurveGroup->add( &m_regressionType ); if ( m_regressionType == RegressionType::POLYNOMINAL ) { regressionCurveGroup->add( &m_polynominalDegree ); } regressionCurveGroup->add( &m_expressionText ); caf::PdmUiGroup* timeSelectionGroup = uiOrdering.addNewGroup( "Time Selection" ); timeSelectionGroup->add( &m_minTimeStep ); timeSelectionGroup->add( &m_maxTimeStep ); timeSelectionGroup->add( &m_showTimeSelectionInPlot ); caf::PdmUiGroup* forecastingGroup = uiOrdering.addNewGroup( "Forecasting" ); forecastingGroup->add( &m_forecastForward ); forecastingGroup->add( &m_forecastBackward ); forecastingGroup->add( &m_forecastUnit ); RimSummaryCurve::defineUiOrdering( uiConfigName, uiOrdering ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSummaryRegressionAnalysisCurve::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) { if ( &m_minTimeStep == changedField && m_minTimeStep > m_maxTimeStep ) { m_maxTimeStep = m_minTimeStep; } if ( &m_maxTimeStep == changedField && m_maxTimeStep < m_minTimeStep ) { m_minTimeStep = m_maxTimeStep; } RimSummaryCurve::fieldChangedByUi( changedField, oldValue, newValue ); if ( changedField == &m_regressionType || changedField == &m_polynominalDegree || changedField == &m_forecastBackward || changedField == &m_forecastForward || changedField == &m_forecastUnit || changedField == &m_minTimeStep || changedField == &m_maxTimeStep || changedField == &m_showTimeSelectionInPlot ) { loadAndUpdateDataAndPlot(); auto plot = firstAncestorOrThisOfTypeAsserted(); if ( plot ) plot->zoomAll(); } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSummaryRegressionAnalysisCurve::defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) { RimSummaryCurve::defineEditorAttribute( field, uiConfigName, attribute ); if ( field == &m_polynominalDegree ) { if ( auto* lineEditorAttr = dynamic_cast( attribute ) ) { // Polynominal degree should be a positive number. 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_minTimeStep || field == &m_maxTimeStep ) { if ( auto* myAttr = dynamic_cast( attribute ) ) { auto timeSteps = RimSummaryCurve::timeStepsY(); if ( !timeSteps.empty() ) { myAttr->m_minimum = *timeSteps.begin(); myAttr->m_maximum = *timeSteps.rbegin(); } myAttr->m_showSpinBox = false; } } else if ( field == &m_expressionText ) { auto myAttr = dynamic_cast( attribute ); if ( myAttr ) { myAttr->wrapMode = caf::PdmUiTextEditorAttribute::NoWrap; myAttr->textMode = caf::PdmUiTextEditorAttribute::HTML; } } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::createCurveAutoName() { return RimSummaryCurve::createCurveAutoName() + " " + m_regressionType().uiText() + " Regression"; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::curveExportDescription( const RifEclipseSummaryAddress& address ) const { return RimSummaryCurve::curveExportDescription() + "." + m_regressionType().uiText() + "_Regression"; } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::formatDouble( double v ) { return QString::number( v, 'g', 2 ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::LinearRegression& reg ) { QString sign = reg.intercept() < 0.0 ? "-" : "+"; return QString( "y = %1x %2 %3" ).arg( formatDouble( reg.slope() ) ).arg( sign ).arg( formatDouble( std::fabs( reg.intercept() ) ) ) + QString( "
R2 = %1" ).arg( reg.r2() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::PolynominalRegression& reg ) { QString str = "y = "; bool isFirst = true; std::vector coeffs = reg.coeffisients(); QStringList parts; for ( size_t i = 0; i < coeffs.size(); i++ ) { double coeff = coeffs[i]; // Skip zero coeffs if ( coeff != 0.0 ) { if ( coeff < 0.0 ) parts.append( "-" ); else if ( !isFirst ) parts.append( "+" ); if ( i == 0 ) { parts.append( QString( "%1" ).arg( formatDouble( std::fabs( coeff ) ) ) ); } else { parts.append( QString( " %1x%2" ).arg( formatDouble( std::fabs( coeff ) ) ).arg( i ) ); } isFirst = false; } } return str + parts.join( " " ) + QString( "
R2 = %1" ).arg( reg.r2() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::PowerFitRegression& reg ) { return QString( "y = %1 + x%2" ).arg( formatDouble( reg.scale() ) ).arg( formatDouble( reg.exponent() ) ) + QString( "
R2 = %1" ).arg( reg.r2() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::ExponentialRegression& reg ) { return QString( "y = %1 * e%2x" ).arg( formatDouble( reg.a() ) ).arg( formatDouble( reg.b() ) ) + QString( "
R2 = %1" ).arg( reg.r2() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::LogarithmicRegression& reg ) { return QString( "y = %1 + %2 * ln(x)" ).arg( formatDouble( reg.a() ) ).arg( formatDouble( reg.b() ) ) + QString( "
R2 = %1" ).arg( reg.r2() ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- QString RimSummaryRegressionAnalysisCurve::generateRegressionText( const regression::LogisticRegression& reg ) { // 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 ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- std::pair, std::vector> RimSummaryRegressionAnalysisCurve::getInRangeValues( const std::vector& timeSteps, const std::vector& values, time_t minTimeStep, time_t maxTimeStep ) { CAF_ASSERT( timeSteps.size() == values.size() ); std::vector filteredTimeSteps; std::vector filteredValues; for ( size_t i = 0; i < timeSteps.size(); i++ ) { time_t timeStep = timeSteps[i]; if ( timeStep >= minTimeStep && timeStep <= maxTimeStep ) { filteredTimeSteps.push_back( timeStep ); filteredValues.push_back( values[i] ); } } return std::make_pair( filteredTimeSteps, filteredValues ); } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSummaryRegressionAnalysisCurve::updateTimeAnnotations() { auto plot = firstAncestorOrThisOfTypeAsserted(); if ( m_timeRangeAnnotation ) plot->removeTimeAnnotation( m_timeRangeAnnotation ); if ( m_showTimeSelectionInPlot ) { m_timeRangeAnnotation = plot->addTimeRangeAnnotation( m_minTimeStep, m_maxTimeStep ); m_timeRangeAnnotation->setColor( color() ); } } //-------------------------------------------------------------------------------------------------- /// //-------------------------------------------------------------------------------------------------- void RimSummaryRegressionAnalysisCurve::updateDefaultValues() { auto timeSteps = RimSummaryCurve::timeStepsY(); if ( !timeSteps.empty() ) { m_minTimeStep = timeSteps.front(); m_maxTimeStep = timeSteps.back(); } }