Add "Decline Curves" functionality.

Adapted from formulas here:
https://petrowiki.spe.org/Production_forecasting_decline_curve_analysis
This commit is contained in:
Kristian Bendiksen
2023-04-26 12:31:57 +02:00
parent bc718db7df
commit c8b402651d
16 changed files with 878 additions and 33 deletions

View File

@@ -603,3 +603,25 @@ std::vector<QDateTime> RiaQDateTimeTools::getTimeStepsWithinSelectedRange( const
return selectedTimeSteps;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::set<QDateTime>
RiaQDateTimeTools::createEvenlyDistributedDatesInInterval( const QDateTime& fromTimeStamp, const QDateTime& toTimeStamp, int numDates )
{
if ( numDates < 2 ) return {};
// Calculate the time step between the two time stamps
qint64 timeStep = ( toTimeStamp.toMSecsSinceEpoch() - fromTimeStamp.toMSecsSinceEpoch() ) / ( static_cast<qint64>( numDates ) - 1 );
// Create a set of evenly spaced datetimes.
std::set<QDateTime> outputDates;
for ( int i = 0; i < numDates; ++i )
{
qint64 targetTime = i * timeStep;
outputDates.insert( RiaQDateTimeTools::addMSecs( fromTimeStamp, targetTime ) );
}
return outputDates;
}

View File

@@ -95,6 +95,8 @@ public:
static QList<caf::PdmOptionItemInfo> createOptionItems( const std::vector<time_t>& timeSteps );
static std::set<QDateTime> createEvenlyDistributedDates( const std::vector<QDateTime>& inputDates, int numDates );
static std::set<QDateTime>
createEvenlyDistributedDatesInInterval( const QDateTime& fromTimeStep, const QDateTime& toTimeStep, int numDates );
static std::vector<QDateTime>
getTimeStepsWithinSelectedRange( const std::vector<QDateTime>& timeSteps, const QDateTime& fromTimeStep, const QDateTime& toTimeStep );

View File

@@ -51,6 +51,7 @@ set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RicOpenSummaryPlotEditorFromMdiAreaFeature.h
${CMAKE_CURRENT_LIST_DIR}/RicNewSummaryTableFeature.h
${CMAKE_CURRENT_LIST_DIR}/RicDuplicateSummaryTableFeature.h
${CMAKE_CURRENT_LIST_DIR}/RicCreateDeclineCurvesFeature.h
)
set(SOURCE_GROUP_SOURCE_FILES
@@ -106,6 +107,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RicOpenSummaryPlotEditorFromMdiAreaFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicNewSummaryTableFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicDuplicateSummaryTableFeature.cpp
${CMAKE_CURRENT_LIST_DIR}/RicCreateDeclineCurvesFeature.cpp
)
list(APPEND COMMAND_CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES})

View File

@@ -0,0 +1,125 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RicCreateDeclineCurvesFeature.h"
#include "RiaSummaryTools.h"
#include "RimSummaryCurve.h"
#include "RimSummaryDeclineCurve.h"
#include "RimSummaryMultiPlot.h"
#include "RimSummaryPlot.h"
#include "RiuPlotMainWindowTools.h"
#include "cafSelectionManagerTools.h"
#include "cvfAssert.h"
#include <QAction>
CAF_CMD_SOURCE_INIT( RicCreateDeclineCurvesFeature, "RicCreateDeclineCurvesFeature" );
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
bool RicCreateDeclineCurvesFeature::isCommandEnabled()
{
RimSummaryPlot* selectedPlot = caf::firstAncestorOfTypeFromSelectedObject<RimSummaryPlot*>();
return ( selectedPlot && !RiaSummaryTools::isSummaryCrossPlot( selectedPlot ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RicCreateDeclineCurvesFeature::onActionTriggered( bool isChecked )
{
RimSummaryCurve* curve = caf::firstAncestorOfTypeFromSelectedObject<RimSummaryCurve*>();
if ( curve )
{
std::vector<RimSummaryDeclineCurve::DeclineCurveType> declineCurveTypes = { RimSummaryDeclineCurve::DeclineCurveType::EXPONENTIAL,
RimSummaryDeclineCurve::DeclineCurveType::HYPERBOLIC,
RimSummaryDeclineCurve::DeclineCurveType::HARMONIC };
for ( auto declineCurveType : declineCurveTypes )
{
RimSummaryDeclineCurve* newCurve = createDeclineCurveAndAddToPlot( curve, declineCurveType );
RiuPlotMainWindowTools::showPlotMainWindow();
RiuPlotMainWindowTools::selectAsCurrentItem( newCurve );
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RicCreateDeclineCurvesFeature::setupActionLook( QAction* actionToSetup )
{
actionToSetup->setText( "Create Decline Curves" );
actionToSetup->setIcon( QIcon( ":/SummaryCurve16x16.png" ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimSummaryDeclineCurve* RicCreateDeclineCurvesFeature::createDeclineCurveAndAddToPlot( RimSummaryCurve* sourceCurve,
RimSummaryDeclineCurve::DeclineCurveType declineCurveType )
{
auto mapToLineStyle = []( RimSummaryDeclineCurve::DeclineCurveType t )
{
if ( t == RimSummaryDeclineCurve::DeclineCurveType::HARMONIC ) return RiuQwtPlotCurveDefines::LineStyleEnum::STYLE_DOT;
if ( t == RimSummaryDeclineCurve::DeclineCurveType::HYPERBOLIC ) return RiuQwtPlotCurveDefines::LineStyleEnum::STYLE_DASH;
return RiuQwtPlotCurveDefines::LineStyleEnum::STYLE_DASH_DOT;
};
RimSummaryPlot* summaryPlot = caf::firstAncestorOfTypeFromSelectedObject<RimSummaryPlot*>();
RimSummaryDeclineCurve* newCurve = new RimSummaryDeclineCurve();
CVF_ASSERT( newCurve );
newCurve->setSummaryCaseX( sourceCurve->summaryCaseX() );
newCurve->setSummaryAddressX( sourceCurve->summaryAddressX() );
newCurve->setSummaryCaseY( sourceCurve->summaryCaseY() );
newCurve->setSummaryAddressY( sourceCurve->summaryAddressY() );
newCurve->setDeclineCurveType( declineCurveType );
newCurve->setColor( sourceCurve->color() );
newCurve->setLineStyle( mapToLineStyle( declineCurveType ) );
summaryPlot->addCurveAndUpdate( newCurve );
newCurve->loadDataAndUpdate( true );
newCurve->updateConnectedEditors();
RimSummaryMultiPlot* summaryMultiPlot = nullptr;
summaryPlot->firstAncestorOrThisOfType( summaryMultiPlot );
if ( summaryMultiPlot )
{
summaryMultiPlot->updatePlotTitles();
}
else
{
summaryPlot->updatePlotTitle();
}
summaryPlot->updateAllRequiredEditors();
return newCurve;
}

View File

@@ -0,0 +1,42 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafCmdFeature.h"
#include "RimSummaryDeclineCurve.h"
class RimSummaryPlot;
class RimSummaryCurve;
//==================================================================================================
///
//==================================================================================================
class RicCreateDeclineCurvesFeature : public caf::CmdFeature
{
CAF_CMD_HEADER_INIT;
protected:
bool isCommandEnabled() override;
void onActionTriggered( bool isChecked ) override;
void setupActionLook( QAction* actionToSetup ) override;
static RimSummaryDeclineCurve* createDeclineCurveAndAddToPlot( RimSummaryCurve* sourceCurve,
RimSummaryDeclineCurve::DeclineCurveType declineCurveType );
};

View File

@@ -717,6 +717,7 @@ caf::CmdFeatureMenuBuilder RimContextCommandBuilder::commandsFromSelection()
menuBuilder << "Separator";
menuBuilder << "RicNewSummaryCurveFeature";
menuBuilder << "RicDuplicateSummaryCurveFeature";
menuBuilder << "RicCreateDeclineCurvesFeature";
menuBuilder << "RicNewSummaryCrossPlotCurveFeature";
menuBuilder << "RicDuplicateSummaryCrossPlotCurveFeature";
menuBuilder << "Separator";

View File

@@ -50,6 +50,7 @@ set(SOURCE_GROUP_HEADER_FILES
${CMAKE_CURRENT_LIST_DIR}/RimSummaryTable.h
${CMAKE_CURRENT_LIST_DIR}/RimSummaryTableCollection.h
${CMAKE_CURRENT_LIST_DIR}/RimSummaryTableTools.h
${CMAKE_CURRENT_LIST_DIR}/RimSummaryDeclineCurve.h
)
set(SOURCE_GROUP_SOURCE_FILES
@@ -104,6 +105,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RimSummaryTable.cpp
${CMAKE_CURRENT_LIST_DIR}/RimSummaryTableCollection.cpp
${CMAKE_CURRENT_LIST_DIR}/RimSummaryTableTools.cpp
${CMAKE_CURRENT_LIST_DIR}/RimSummaryDeclineCurve.cpp
)
list(APPEND CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES})

View File

@@ -1233,19 +1233,31 @@ void RimSummaryCurve::fieldChangedByUi( const caf::PdmFieldHandle* changedField,
if ( loadAndUpdate )
{
this->loadDataAndUpdate( true );
plot->updateAxes();
plot->updatePlotTitle();
plot->updateConnectedEditors();
RiuPlotMainWindow* mainPlotWindow = RiaGuiApplication::instance()->mainPlotWindow();
mainPlotWindow->updateMultiPlotToolBar();
dataChanged.send();
loadAndUpdateDataAndPlot();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryCurve::loadAndUpdateDataAndPlot()
{
this->loadDataAndUpdate( true );
RimSummaryPlot* plot = nullptr;
firstAncestorOrThisOfType( plot );
CVF_ASSERT( plot );
plot->updateAxes();
plot->updatePlotTitle();
plot->updateConnectedEditors();
RiuPlotMainWindow* mainPlotWindow = RiaGuiApplication::instance()->mainPlotWindow();
mainPlotWindow->updateMultiPlotToolBar();
dataChanged.send();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------

View File

@@ -53,11 +53,11 @@ public:
~RimSummaryCurve() override;
// Y Axis functions
RiaSummaryCurveDefinition curveDefinitionY() const;
RimSummaryCase* summaryCaseY() const;
RifEclipseSummaryAddress summaryAddressY() const;
std::string unitNameY() const;
std::vector<double> valuesY() const;
RiaSummaryCurveDefinition curveDefinitionY() const;
RimSummaryCase* summaryCaseY() const;
RifEclipseSummaryAddress summaryAddressY() const;
std::string unitNameY() const;
virtual std::vector<double> valuesY() const;
void applyCurveDefinitionY( const RiaSummaryCurveDefinition& curveDefinition );
void setSummaryCaseY( RimSummaryCase* sumCase );
@@ -65,20 +65,20 @@ public:
void setSummaryAddressY( const RifEclipseSummaryAddress& address );
void setResampling( RiaDefines::DateTimePeriodEnum resampling );
RifEclipseSummaryAddress errorSummaryAddressY() const;
std::vector<double> errorValuesY() const;
void setLeftOrRightAxisY( RiuPlotAxis plotAxis );
RiuPlotAxis axisY() const;
std::vector<time_t> timeStepsY() const;
double yValueAtTimeT( time_t time ) const;
void setOverrideCurveDataY( const std::vector<time_t>& xValues, const std::vector<double>& yValues );
RifEclipseSummaryAddress errorSummaryAddressY() const;
std::vector<double> errorValuesY() const;
void setLeftOrRightAxisY( RiuPlotAxis plotAxis );
RiuPlotAxis axisY() const;
virtual std::vector<time_t> timeStepsY() const;
double yValueAtTimeT( time_t time ) const;
void setOverrideCurveDataY( const std::vector<time_t>& xValues, const std::vector<double>& yValues );
// X Axis functions
RiaSummaryCurveDefinition curveDefinitionX() const;
RimSummaryCase* summaryCaseX() const;
RifEclipseSummaryAddress summaryAddressX() const;
std::string unitNameX() const;
std::vector<double> valuesX() const;
RiaSummaryCurveDefinition curveDefinitionX() const;
RimSummaryCase* summaryCaseX() const;
RifEclipseSummaryAddress summaryAddressX() const;
std::string unitNameX() const;
virtual std::vector<double> valuesX() const;
void setSummaryCaseX( RimSummaryCase* sumCase );
void setSummaryAddressX( const RifEclipseSummaryAddress& address );
@@ -105,18 +105,15 @@ protected:
void updateZoomInParentPlot() override;
void onLoadDataAndUpdate( bool updateParentPlot ) override;
void loadAndUpdateDataAndPlot();
void updateLegendsInPlot() override;
void defineUiTreeOrdering( caf::PdmUiTreeOrdering& uiTreeOrdering, QString uiConfigName = "" ) override;
void initAfterRead() override;
double computeCurveZValue() override;
private:
RifSummaryReaderInterface* valuesSummaryReaderX() const;
RifSummaryReaderInterface* valuesSummaryReaderY() const;
std::vector<time_t> timeStepsX() const;
void calculateCurveInterpolationFromAddress();
virtual std::vector<time_t> timeStepsX() const;
// Overridden PDM methods
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
@@ -124,6 +121,12 @@ private:
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
void defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override;
private:
RifSummaryReaderInterface* valuesSummaryReaderX() const;
RifSummaryReaderInterface* valuesSummaryReaderY() const;
void calculateCurveInterpolationFromAddress();
static void appendOptionItemsForSummaryAddresses( QList<caf::PdmOptionItemInfo>* options, RimSummaryCase* summaryCase );
private:

View File

@@ -0,0 +1,343 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RimSummaryDeclineCurve.h"
#include "RiaQDateTimeTools.h"
#include "RiaSummaryTools.h"
#include "RiaTimeTTools.h"
#include "RigDeclineCurveCalculator.h"
#include "cafPdmUiDoubleSliderEditor.h"
#include "cafPdmUiLineEditor.h"
#include <QDateTime>
#include <cmath>
CAF_PDM_SOURCE_INIT( RimSummaryDeclineCurve, "DeclineCurve" );
namespace caf
{
template <>
void caf::AppEnum<RimSummaryDeclineCurve::DeclineCurveType>::setUp()
{
addItem( RimSummaryDeclineCurve::DeclineCurveType::EXPONENTIAL, "EXPONENTIAL", "Exponential" );
addItem( RimSummaryDeclineCurve::DeclineCurveType::HARMONIC, "HARMONIC", "Harmonic" );
addItem( RimSummaryDeclineCurve::DeclineCurveType::HYPERBOLIC, "HYPERBOLIC", "Hyperbolic" );
setDefault( RimSummaryDeclineCurve::DeclineCurveType::HARMONIC );
}
}; // namespace caf
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimSummaryDeclineCurve::RimSummaryDeclineCurve()
{
CAF_PDM_InitObject( "Decline Curve", ":/SummaryCurve16x16.png" );
CAF_PDM_InitFieldNoDefault( &m_declineCurveType, "DeclineCurveType", "Type" );
CAF_PDM_InitField( &m_predictionYears, "PredictionYears", 5, "Years" );
CAF_PDM_InitField( &m_hyperbolicDeclineConstant, "HyperbolicDeclineConstant", 0.5, "Decline Constant" );
m_hyperbolicDeclineConstant.uiCapability()->setUiEditorTypeName( caf::PdmUiDoubleSliderEditor::uiEditorTypeName() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
RimSummaryDeclineCurve::~RimSummaryDeclineCurve()
{
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<double> RimSummaryDeclineCurve::valuesY() const
{
return createDeclineCurveValues( RimSummaryCurve::valuesY(),
RimSummaryCurve::timeStepsY(),
RiaSummaryTools::hasAccumulatedData( summaryAddressY() ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<double> RimSummaryDeclineCurve::valuesX() const
{
return createDeclineCurveValues( RimSummaryCurve::valuesX(),
RimSummaryCurve::timeStepsX(),
RiaSummaryTools::hasAccumulatedData( summaryAddressX() ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<time_t> RimSummaryDeclineCurve::timeStepsY() const
{
std::vector<time_t> timeSteps = RimSummaryCurve::timeStepsY();
appendFutureTimeSteps( timeSteps );
return timeSteps;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<time_t> RimSummaryDeclineCurve::timeStepsX() const
{
std::vector<time_t> timeSteps = RimSummaryCurve::timeStepsX();
appendFutureTimeSteps( timeSteps );
return timeSteps;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::vector<double> RimSummaryDeclineCurve::createDeclineCurveValues( const std::vector<double>& values,
const std::vector<time_t>& timeSteps,
bool isAccumulatedResult ) const
{
if ( values.empty() ) return values;
if ( timeSteps.empty() ) return values;
auto [initialProductionRate, initialDeclineRate] = computeInitialProductionAndDeclineRate( values, timeSteps, isAccumulatedResult );
if ( std::isinf( initialProductionRate ) || std::isnan( initialProductionRate ) || std::isinf( initialDeclineRate ) ||
std::isnan( initialDeclineRate ) )
{
return values;
}
QDateTime initialTime = RiaQDateTimeTools::fromTime_t( timeSteps.back() );
std::set<QDateTime> futureTimeSteps = createFutureTimeSteps( timeSteps );
std::vector<double> outValues = values;
for ( const QDateTime& futureTime : futureTimeSteps )
{
double timeSinceStart = futureTime.toSecsSinceEpoch() - initialTime.toSecsSinceEpoch();
double predictedValue = computePredictedValue( initialProductionRate, initialDeclineRate, timeSinceStart, isAccumulatedResult );
if ( isAccumulatedResult ) predictedValue += values.back();
outValues.push_back( predictedValue );
}
return outValues;
}
std::pair<double, double> RimSummaryDeclineCurve::computeInitialProductionAndDeclineRate( const std::vector<double>& values,
const std::vector<time_t>& timeSteps,
bool isAccumulatedResult )
{
auto computeProductionRate = []( double t0, double v0, double t1, double v1 ) { return ( v1 - v0 ) / ( t1 - t0 ); };
const double historyStep = 0.25;
// Select a point a 1/4 back in the existing curve.
const size_t idx0 = static_cast<size_t>( timeSteps.size() * ( 1.0 - historyStep ) );
const QDateTime t0 = RiaQDateTimeTools::fromTime_t( timeSteps[idx0] );
const double v0 = values[idx0];
const QDateTime initialTime = RiaQDateTimeTools::fromTime_t( timeSteps.back() );
if ( !isAccumulatedResult )
{
// Last point on the existing curve is the initial production rate (for non-accumulated data).
double initialProductionRate = values.back();
// Compute the decline rate using the rates at the two points
double initialDeclineRate =
RigDeclineCurveCalculator::computeDeclineRate( t0.toSecsSinceEpoch(), v0, initialTime.toSecsSinceEpoch(), initialProductionRate );
return { initialProductionRate, initialDeclineRate };
}
else
{
// For accumulated result: compute the initial production rate from the two points.
double initialProductionRate = computeProductionRate( t0.toSecsSinceEpoch(), v0, initialTime.toSecsSinceEpoch(), values.back() );
// Compute the at production rate at time t0 by using a point even further back in the existing curve.
size_t idxX = static_cast<size_t>( timeSteps.size() * ( 1.0 - ( historyStep * 2 ) ) );
QDateTime tx = RiaQDateTimeTools::fromTime_t( timeSteps[idxX] );
double vx = values[idxX];
double productionRate0 = computeProductionRate( tx.toSecsSinceEpoch(), vx, t0.toSecsSinceEpoch(), v0 );
// Compute the decline rate using the rates at the two points
double initialDeclineRate = RigDeclineCurveCalculator::computeDeclineRate( t0.toSecsSinceEpoch(),
productionRate0,
initialTime.toSecsSinceEpoch(),
initialProductionRate );
return { initialProductionRate, initialDeclineRate };
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double RimSummaryDeclineCurve::computePredictedValue( double initialProductionRate,
double initialDeclineRate,
double timeSinceStart,
bool isAccumulatedResult ) const
{
if ( isAccumulatedResult )
{
if ( m_declineCurveType == RimSummaryDeclineCurve::DeclineCurveType::EXPONENTIAL )
{
return RigDeclineCurveCalculator::computeCumulativeProductionExponentialDecline( initialProductionRate,
initialDeclineRate,
timeSinceStart );
}
else if ( m_declineCurveType == RimSummaryDeclineCurve::DeclineCurveType::HARMONIC )
{
return RigDeclineCurveCalculator::computeCumulativeProductionHarmonicDecline( initialProductionRate,
initialDeclineRate,
timeSinceStart );
}
else if ( m_declineCurveType == RimSummaryDeclineCurve::DeclineCurveType::HYPERBOLIC )
{
return RigDeclineCurveCalculator::computeCumulativeProductionHyperbolicDecline( initialProductionRate,
initialDeclineRate,
timeSinceStart,
m_hyperbolicDeclineConstant );
}
}
else
{
if ( m_declineCurveType == RimSummaryDeclineCurve::DeclineCurveType::EXPONENTIAL )
{
return RigDeclineCurveCalculator::computeFlowRateExponentialDecline( initialProductionRate, initialDeclineRate, timeSinceStart );
}
else if ( m_declineCurveType == RimSummaryDeclineCurve::DeclineCurveType::HARMONIC )
{
return RigDeclineCurveCalculator::computeFlowRateHarmonicDecline( initialProductionRate, initialDeclineRate, timeSinceStart );
}
else if ( m_declineCurveType == RimSummaryDeclineCurve::DeclineCurveType::HYPERBOLIC )
{
return RigDeclineCurveCalculator::computeFlowRateHyperbolicDecline( initialProductionRate,
initialDeclineRate,
timeSinceStart,
m_hyperbolicDeclineConstant );
}
}
return 0.0;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
std::set<QDateTime> RimSummaryDeclineCurve::createFutureTimeSteps( const std::vector<time_t>& timeSteps ) const
{
if ( timeSteps.empty() ) return {};
// Create additional time steps
QDateTime lastTimeStep = RiaQDateTimeTools::fromTime_t( timeSteps.back() );
QDateTime predictionEnd = RiaQDateTimeTools::addYears( lastTimeStep, m_predictionYears() );
int numDates = 50;
return RiaQDateTimeTools::createEvenlyDistributedDatesInInterval( lastTimeStep, predictionEnd, numDates );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryDeclineCurve::appendFutureTimeSteps( std::vector<time_t>& timeSteps ) const
{
std::set<QDateTime> futureTimeSteps = createFutureTimeSteps( timeSteps );
appendTimeSteps( timeSteps, futureTimeSteps );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryDeclineCurve::appendTimeSteps( std::vector<time_t>& timeSteps, const std::set<QDateTime>& moreTimeSteps )
{
for ( const QDateTime& t : moreTimeSteps )
timeSteps.push_back( RiaTimeTTools::fromQDateTime( t ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryDeclineCurve::defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering )
{
RimPlotCurve::updateFieldUiState();
caf::PdmUiGroup* declineCurveGroup = uiOrdering.addNewGroup( "Decline Curve" );
declineCurveGroup->add( &m_declineCurveType );
declineCurveGroup->add( &m_predictionYears );
if ( m_declineCurveType == RimSummaryDeclineCurve::DeclineCurveType::HYPERBOLIC )
{
declineCurveGroup->add( &m_hyperbolicDeclineConstant );
}
RimSummaryCurve::defineUiOrdering( uiConfigName, uiOrdering );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryDeclineCurve::fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue )
{
RimSummaryCurve::fieldChangedByUi( changedField, oldValue, newValue );
if ( changedField == &m_declineCurveType || changedField == &m_predictionYears || changedField == &m_hyperbolicDeclineConstant )
{
loadAndUpdateDataAndPlot();
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryDeclineCurve::defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute )
{
RimSummaryCurve::defineEditorAttribute( field, uiConfigName, attribute );
if ( field == &m_predictionYears )
{
if ( auto* lineEditorAttr = dynamic_cast<caf::PdmUiLineEditorAttribute*>( attribute ) )
{
// Predict into the future should be a positive number.
lineEditorAttr->validator = new QIntValidator( 1, 50, nullptr );
}
}
if ( field == &m_hyperbolicDeclineConstant )
{
if ( auto* myAttr = dynamic_cast<caf::PdmUiDoubleSliderEditorAttribute*>( attribute ) )
{
// Hyperbolic decline constant must be larger than 0 to avoid calculation issues.
myAttr->m_minimum = 0.001;
myAttr->m_maximum = 1.0;
myAttr->m_decimals = 2;
}
}
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
void RimSummaryDeclineCurve::setDeclineCurveType( DeclineCurveType declineCurveType )
{
m_declineCurveType = declineCurveType;
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
QString RimSummaryDeclineCurve::createCurveAutoName()
{
return RimSummaryCurve::createCurveAutoName() + " " + m_declineCurveType().uiText() + " Decline";
}

View File

@@ -0,0 +1,85 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
#include "cafPdmField.h"
#include "cafPdmObject.h"
#include "RimSummaryCurve.h"
#include "cafAppEnum.h"
#include <utility>
//==================================================================================================
///
///
//==================================================================================================
class RimSummaryDeclineCurve : public RimSummaryCurve
{
CAF_PDM_HEADER_INIT;
public:
enum class DeclineCurveType
{
EXPONENTIAL,
HARMONIC,
HYPERBOLIC
};
RimSummaryDeclineCurve();
~RimSummaryDeclineCurve() override;
void setDeclineCurveType( DeclineCurveType declineCurveType );
// Y Axis functions
std::vector<double> valuesY() const override;
std::vector<time_t> timeStepsY() const override;
// X Axis functions
std::vector<double> valuesX() const override;
private:
QString createCurveAutoName() override;
std::vector<time_t> timeStepsX() const override;
// Overridden PDM methods
void fieldChangedByUi( const caf::PdmFieldHandle* changedField, const QVariant& oldValue, const QVariant& newValue ) override;
void defineUiOrdering( QString uiConfigName, caf::PdmUiOrdering& uiOrdering ) override;
void defineEditorAttribute( const caf::PdmFieldHandle* field, QString uiConfigName, caf::PdmUiEditorAttribute* attribute ) override;
void appendFutureTimeSteps( std::vector<time_t>& timeSteps ) const;
std::vector<double>
createDeclineCurveValues( const std::vector<double>& values, const std::vector<time_t>& timeSteps, bool isAccumulatedResult ) const;
std::set<QDateTime> createFutureTimeSteps( const std::vector<time_t>& timeSteps ) const;
static void appendTimeSteps( std::vector<time_t>& timeSteps, const std::set<QDateTime>& moreTimeSteps );
static std::pair<double, double> computeInitialProductionAndDeclineRate( const std::vector<double>& values,
const std::vector<time_t>& timeSteps,
bool isAccumulatedResult );
double computePredictedValue( double initialProductionRate, double initialDeclineRate, double timeSinceStart, bool isAccumulatedResult ) const;
caf::PdmField<caf::AppEnum<DeclineCurveType>> m_declineCurveType;
caf::PdmField<int> m_predictionYears;
caf::PdmField<double> m_hyperbolicDeclineConstant;
};

View File

@@ -181,6 +181,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RigWellAllocationOverTime.cpp
${CMAKE_CURRENT_LIST_DIR}/RigWellResultBranch.cpp
${CMAKE_CURRENT_LIST_DIR}/RigWellResultFrame.cpp
${CMAKE_CURRENT_LIST_DIR}/RigDeclineCurveCalculator.cpp
)
list(APPEND CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES})

View File

@@ -0,0 +1,97 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "RigDeclineCurveCalculator.h"
#include <cmath>
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double RigDeclineCurveCalculator::computeDeclineRate( double time0, double value0, double time1, double value1 )
{
return ( 1.0 / ( time1 - time0 ) ) * std::log( value0 / value1 );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double RigDeclineCurveCalculator::computeFlowRateExponentialDecline( double initialProductionRateQi,
double initialDeclineRateDi,
double timeSinceStart )
{
return initialProductionRateQi * std::exp( -initialDeclineRateDi * timeSinceStart );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double RigDeclineCurveCalculator::computeFlowRateHarmonicDecline( double initialProductionRateQi, double initialDeclineRateDi, double timeSinceStart )
{
return initialProductionRateQi / ( 1.0 + initialDeclineRateDi * timeSinceStart );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double RigDeclineCurveCalculator::computeFlowRateHyperbolicDecline( double initialProductionRateQi,
double initialDeclineRateDi,
double timeSinceStart,
double hyperbolicDeclineConstantB )
{
return initialProductionRateQi /
std::pow( 1.0 + hyperbolicDeclineConstantB * initialDeclineRateDi * timeSinceStart, 1.0 / hyperbolicDeclineConstantB );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double RigDeclineCurveCalculator::computeCumulativeProductionExponentialDecline( double initialProductionRateQi,
double initialDeclineRateDi,
double timeSinceStart )
{
double productionRate = computeFlowRateExponentialDecline( initialProductionRateQi, initialDeclineRateDi, timeSinceStart );
return ( 1 / initialDeclineRateDi ) * ( initialProductionRateQi - productionRate );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double RigDeclineCurveCalculator::computeCumulativeProductionHarmonicDecline( double initialProductionRateQi,
double initialDeclineRateDi,
double timeSinceStart )
{
double productionRate = computeFlowRateHarmonicDecline( initialProductionRateQi, initialDeclineRateDi, timeSinceStart );
return ( initialProductionRateQi / initialDeclineRateDi ) * std::log( initialProductionRateQi / productionRate );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
double RigDeclineCurveCalculator::computeCumulativeProductionHyperbolicDecline( double initialProductionRateQi,
double initialDeclineRateDi,
double timeSinceStart,
double hyperbolicDeclineConstantB )
{
double productionRate =
computeFlowRateHyperbolicDecline( initialProductionRateQi, initialDeclineRateDi, timeSinceStart, hyperbolicDeclineConstantB );
return ( std::pow( initialProductionRateQi, hyperbolicDeclineConstantB ) / ( ( 1.0 - hyperbolicDeclineConstantB ) * initialDeclineRateDi ) ) *
( std::pow( initialProductionRateQi, 1.0 - hyperbolicDeclineConstantB ) -
std::pow( productionRate, 1.0 - hyperbolicDeclineConstantB ) );
}

View File

@@ -0,0 +1,50 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#pragma once
//==================================================================================================
//
//
//
//==================================================================================================
class RigDeclineCurveCalculator
{
public:
static double computeDeclineRate( double time0, double value0, double time1, double value1 );
static double computeFlowRateExponentialDecline( double initialProductionRateQi, double initialDeclineRateDi, double timeSinceStart );
static double computeFlowRateHarmonicDecline( double initialProductionRateQi, double initialDeclineRateDi, double timeSinceStart );
static double computeFlowRateHyperbolicDecline( double initialProductionRateQi,
double initialDeclineRateDi,
double timeSinceStart,
double hyperbolicDeclineConstantB );
static double
computeCumulativeProductionExponentialDecline( double initialProductionRateQi, double initialDeclineRateDi, double timeSinceStart );
static double
computeCumulativeProductionHarmonicDecline( double initialProductionRateQi, double initialDeclineRateDi, double timeSinceStart );
static double computeCumulativeProductionHyperbolicDecline( double initialProductionRateQi,
double initialDeclineRateDi,
double timeSinceStart,
double hyperbolicDeclineConstantB );
};

View File

@@ -90,6 +90,7 @@ set(SOURCE_GROUP_SOURCE_FILES
${CMAKE_CURRENT_LIST_DIR}/RifRevealCsvSummaryReader-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RifStimPlanCsvSummaryReader-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaEnsembleNameTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RigDeclineCurveCalculator-Test.cpp
)
if(RESINSIGHT_ENABLE_GRPC)

View File

@@ -0,0 +1,57 @@
/////////////////////////////////////////////////////////////////////////////////
//
// 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 <http://www.gnu.org/licenses/gpl.html>
// for more details.
//
/////////////////////////////////////////////////////////////////////////////////
#include "gtest/gtest.h"
#include "RigDeclineCurveCalculator.h"
// Test case from https://web.archive.org/web/20120710104333/http://infohost.nmt.edu/~petro/faculty/Kelly/Deline.pdf
// Example Problem 8-1:
// "Given that a well has declined from 100 stb/day to 96 stb/day during a one-month period,
// use the exponential decline model to perform the following tasks:
// Predict the production rate after 11 more months."
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RigDeclineCurveCalculatorTests, computeDeclineRateExample )
{
double t0 = 0.0;
double q0 = 100.0;
double t1 = 1.0;
double q1 = 96.0;
double expectedDeclineRateDi = 0.04082;
double declineRate = RigDeclineCurveCalculator::computeDeclineRate( t0, q0, t1, q1 );
EXPECT_NEAR( expectedDeclineRateDi, declineRate, 0.005 );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RigDeclineCurveCalculatorTests, exponentialFlowRateExample )
{
double initialProductionRateQi = 96.0;
double initialDeclineRateDi = 0.04082;
double timeSinceStart = 11.0;
double q = RigDeclineCurveCalculator::computeFlowRateExponentialDecline( initialProductionRateQi, initialDeclineRateDi, timeSinceStart );
EXPECT_NEAR( q, 61.27, 0.005 );
}