#15021: Add unit tests for completion export and non-Darcy D-factor

This commit is contained in:
Magne Sjaastad committed 2026-10-24 20:30:43 +02:00
1 parent 1e380d0a03
commit a6c2b1b911
7 files changed
+970 -2

No files matched your search

@@ -77,6 +77,9 @@ set(SOURCE_UNITTEST_FILES
${CMAKE_CURRENT_LIST_DIR}/RifElementPropertyTableReader-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimRelocatePath-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RigTransmissibilityCondenser-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RigPerforationTransmissibilityEquations-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimFractureTemplateNonDarcy-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RimStimPlanFractureTemplate-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaEclipseUnitTools-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RiaTextFileCompare-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RifCaseRealizationParametersReader-Test.cpp
@@ -220,6 +223,7 @@ set(SOURCE_UNITTEST_FILES
${CMAKE_CURRENT_LIST_DIR}/RigAllGridCellsResultAccessor-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RicScheduleDataGenerator-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RicWellPathExportCompletionDataFeatureImpl-Test.cpp
${CMAKE_CURRENT_LIST_DIR}/RicCompletionDataExportTools-Test.cpp
)
if(RESINSIGHT_ENABLE_GRPC)
@@ -0,0 +1,227 @@
#include "gtest/gtest.h"
#include "CompletionExportCommands/RicCompletionDataExportTools.h"
#include "CompletionExportCommands/RicExportCompletionDataSettingsUi.h"
#include "RifTextDataTableFormatter.h"
#include "RigCompletionData.h"
#include <QRegularExpression>
#include <QString>
#include <QTextStream>
#include <cmath>
namespace
{
RigCompletionData
makePerforation( double transmissibility, double diameter, double skin, double dFactor, double startMD = 100.0, double endMD = 110.0 )
{
RigCompletionData completion( "W", RigCompletionDataGridCell(), startMD );
completion.setTransAndWPImultBackgroundDataFromPerforation( transmissibility,
skin,
diameter,
dFactor,
RigCompletionData::defaultValue(),
RigCompletionData::CellDirection::DIR_K );
completion.setDepthRange( startMD, endMD );
return completion;
}
// Owns the PDM object locally, so it is not alive after the test environment is torn down
struct ExplicitTransSettings
{
ExplicitTransSettings() { m_settings.compdatExport = RicExportCompletionDataSettingsUi::CompdatExport::TRANSMISSIBILITIES; }
operator const RicExportCompletionDataSettingsUi&() const { return m_settings; }
RicExportCompletionDataSettingsUi m_settings;
};
ExplicitTransSettings explicitTransSettings()
{
return {};
}
QString exportTable( void ( *exporter )( RifTextDataTableFormatter&, const QString&, const std::vector<RigCompletionData>& ),
const std::vector<RigCompletionData>& completions )
{
QString text;
QTextStream stream( &text );
RifTextDataTableFormatter formatter( stream );
exporter( formatter, QString(), completions );
return text;
}
} // namespace
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, CombineSumsTransmissibilityAndWeightsValues )
{
const auto combined = RicCompletionDataExportTools::combineEclipseCellCompletions( { makePerforation( 1.0, 0.2, 1.0, 2.0 ),
makePerforation( 3.0, 0.4, 5.0, 4.0 ) },
explicitTransSettings() );
EXPECT_DOUBLE_EQ( 4.0, combined.transmissibility() );
EXPECT_DOUBLE_EQ( ( 0.2 * 1.0 + 0.4 * 3.0 ) / 4.0, combined.diameter() );
EXPECT_DOUBLE_EQ( ( 1.0 * 1.0 + 5.0 * 3.0 ) / 4.0, combined.skinFactor() );
EXPECT_EQ( RigCompletionData::CompletionType::PERFORATION, combined.completionType() );
}
//--------------------------------------------------------------------------------------------------
/// The combined D-factor is the transmissibility weighted mean divided by the number of completions
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, CombineDFactorIsWeightedMeanDividedByCount )
{
const auto combined = RicCompletionDataExportTools::combineEclipseCellCompletions( { makePerforation( 1.0, 0.2, 0.0, 2.0 ),
makePerforation( 3.0, 0.2, 0.0, 4.0 ) },
explicitTransSettings() );
EXPECT_DOUBLE_EQ( ( 2.0 * 1.0 + 4.0 * 3.0 ) / 4.0 / 2.0, combined.dFactor() );
const auto single =
RicCompletionDataExportTools::combineEclipseCellCompletions( { makePerforation( 2.0, 0.2, 0.0, 3.0 ) }, explicitTransSettings() );
EXPECT_DOUBLE_EQ( 3.0, single.dFactor() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, CombineMergesMeasuredDepthRange )
{
const auto combined = RicCompletionDataExportTools::combineEclipseCellCompletions( { makePerforation( 1.0, 0.2, 0.0, 1.0, 100.0, 110.0 ),
makePerforation( 1.0, 0.2, 0.0, 1.0, 105.0, 130.0 ) },
explicitTransSettings() );
ASSERT_TRUE( combined.startMD().has_value() );
ASSERT_TRUE( combined.endMD().has_value() );
EXPECT_DOUBLE_EQ( 100.0, combined.startMD().value() );
EXPECT_DOUBLE_EQ( 130.0, combined.endMD().value() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, CombineIgnoresNegativeTransmissibility )
{
const auto combined = RicCompletionDataExportTools::combineEclipseCellCompletions( { makePerforation( 2.0, 0.2, 0.0, 1.0 ),
makePerforation( -1.0, 0.2, 0.0, 1.0 ) },
explicitTransSettings() );
EXPECT_DOUBLE_EQ( 2.0, combined.transmissibility() );
// Skipped completions must not count in the divisor of the combined D-factor
EXPECT_DOUBLE_EQ( 1.0, combined.dFactor() );
ASSERT_FALSE( combined.metadata().empty() );
EXPECT_EQ( "ERROR", combined.metadata().front().name );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, CombineDifferentWellsGivesUnexportedCompletion )
{
RigCompletionData other = makePerforation( 1.0, 0.2, 0.0, 1.0 );
RigCompletionData otherWell( "OTHER", RigCompletionDataGridCell(), 100.0 );
otherWell.setTransAndWPImultBackgroundDataFromPerforation( 1.0, 0.0, 0.2, 1.0, 1.0, RigCompletionData::CellDirection::DIR_K );
const auto combined = RicCompletionDataExportTools::combineEclipseCellCompletions( { other, otherWell }, explicitTransSettings() );
EXPECT_TRUE( RigCompletionData::isDefaultValue( combined.transmissibility() ) );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, CombineIgnoresInfiniteTransmissibility )
{
const auto combined = RicCompletionDataExportTools::combineEclipseCellCompletions( { makePerforation( HUGE_VAL, 0.2, 0.0, 1.0 ) },
explicitTransSettings() );
EXPECT_DOUBLE_EQ( 0.0, combined.transmissibility() );
ASSERT_FALSE( combined.metadata().empty() );
EXPECT_EQ( "ERROR", combined.metadata().front().name );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, MainGridCompletionsKeepsCellsWithoutLgr )
{
std::vector<RigCompletionData> completions = { makePerforation( 1.0, 0.2, 0.0, 1.0 ), makePerforation( 2.0, 0.2, 0.0, 1.0 ) };
EXPECT_EQ( 2u, RicCompletionDataExportTools::mainGridCompletions( completions ).size() );
EXPECT_TRUE( RicCompletionDataExportTools::subGridsCompletions( completions ).empty() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, AppendCompletionDataGroupsByGlobalCellIndex )
{
std::map<size_t, std::vector<RigCompletionData>> grouped;
RicCompletionDataExportTools::appendCompletionData( &grouped,
{ makePerforation( 1.0, 0.2, 0.0, 1.0 ), makePerforation( 2.0, 0.2, 0.0, 1.0 ) } );
RicCompletionDataExportTools::appendCompletionData( &grouped, { makePerforation( 3.0, 0.2, 0.0, 1.0 ) } );
ASSERT_EQ( 1u, grouped.size() );
EXPECT_EQ( 3u, grouped.begin()->second.size() );
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, CompdatUsesDefaultMarkerWithoutDFactor )
{
const QString text = exportTable( RicCompletionDataExportTools::exportCompdatTableUsingFormatter,
{ makePerforation( 1.5, 0.2, 0.0, RigCompletionData::defaultValue() ) } );
EXPECT_TRUE( text.contains( "COMPDAT" ) ) << text.toStdString();
EXPECT_TRUE( text.contains( "'Z'" ) ) << text.toStdString();
// The D-factor column is the last one before the direction and is defaulted
EXPECT_TRUE( text.contains( QRegularExpression( "1\\*\\s+'Z'" ) ) ) << text.toStdString();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, CompdatWritesDFactorWhenSet )
{
auto completion = makePerforation( 1.5, 0.2, 0.0, 2.5 );
completion.setPerConnectionDfactor();
const QString text = exportTable( RicCompletionDataExportTools::exportCompdatTableUsingFormatter, { completion } );
EXPECT_FALSE( text.contains( QRegularExpression( "1\\*\\s+'Z'" ) ) ) << text.toStdString();
EXPECT_TRUE( text.contains( "-2.5" ) ) << text.toStdString();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, CompdatSkipsCompletionsWithoutTransmissibility )
{
const QString text =
exportTable( RicCompletionDataExportTools::exportCompdatTableUsingFormatter, { makePerforation( 0.0, 0.2, 0.0, 1.0 ) } );
EXPECT_FALSE( text.contains( "'Z'" ) ) << text.toStdString();
}
//--------------------------------------------------------------------------------------------------
///
//--------------------------------------------------------------------------------------------------
TEST( RicCompletionDataExportTools, ComplumpOnlyExportsCompletionsWithNumber )
{
auto withNumber = makePerforation( 1.5, 0.2, 0.0, 1.0 );
withNumber.setCompletionNumber( 7 );
auto withoutNumber = makePerforation( 1.5, 0.2, 0.0, 1.0 );
const QString text = exportTable( RicCompletionDataExportTools::exportComplumpTableUsingFormatter, { withoutNumber } );
EXPECT_FALSE( text.contains( "7" ) ) << text.toStdString();
const QString textWithNumber = exportTable( RicCompletionDataExportTools::exportComplumpTableUsingFormatter, { withNumber } );
EXPECT_TRUE( textWithNumber.contains( "COMPLUMP" ) );
EXPECT_TRUE( textWithNumber.contains( QRegularExpression( "\\s7\\s" ) ) ) << textWithNumber.toStdString();
}
@@ -20,13 +20,143 @@
#include "Commands/CompletionExportCommands/RicWellPathExportCompletionDataFeatureImpl.h"
#include "CompletionExportCommands/RicCompletionDataExportTools.h"
#include "CompletionExportCommands/RicTransmissibilityCalculator.h"
#include "RiaEclipseUnitTools.h"
#include "RiaFractureDefines.h"
#include "RifReaderMockModel.h"
#include "RigActiveCellInfo.h"
#include "RigCaseCellResultsData.h"
#include "RigCompletionData.h"
#include "RigEclipseCaseData.h"
#include "RigEclipseResultAddress.h"
#include "RigEclipseResultInfo.h"
#include "RigMainGrid.h"
#include "RigPerforationTransmissibilityEquations.h"
#include "Well/RigWellPath.h"
#include "RimEclipseResultCase.h"
#include "RimNonDarcyPerforationParameters.h"
#include "RimOilField.h"
#include "RimPerforationCollection.h"
#include "RimPerforationInterval.h"
#include "RimProject.h"
#include "RimWellPath.h"
#include "RimWellPathCollection.h"
#include "cafPdmField.h"
#include <algorithm>
#include <cmath>
#include <optional>
namespace
{
void addStaticResult( RigCaseCellResultsData* cellResults, const QString& name, size_t cellCount, double value )
{
RigEclipseResultAddress address( RiaDefines::ResultCatType::STATIC_NATIVE, name );
cellResults->createResultEntry( address, false );
cellResults->setTimeStepInfos( address, { RigEclipseTimeStepInfo( QDateTime(), 0, 0.0 ) } );
auto* timesteps = cellResults->modifiableCellScalarResultTimesteps( address );
timesteps->resize( 1 );
( *timesteps )[0].assign( cellCount, value );
}
// 2x2x2 grid with 50 unit cells, z = 0 at the top. PERMX = PERMY = 100 mD, PERMZ = 10 mD, NTG = 1, PORO = 0.2
RimEclipseResultCase* createMockEclipseCase( RiaDefines::EclipseUnitSystem unitSystem )
{
auto* eclipseCase = new RimEclipseResultCase;
cvf::ref<RigEclipseCaseData> caseData = new RigEclipseCaseData( eclipseCase );
caseData->setUnitsType( unitSystem );
cvf::ref<RifReaderMockModel> mockReader = new RifReaderMockModel;
mockReader->setWorldCoordinates( cvf::Vec3d( 0, 0, -100 ), cvf::Vec3d( 100, 100, 0 ) );
mockReader->setCellCounts( cvf::Vec3st( 2, 2, 2 ) );
mockReader->enableWellData( false );
mockReader->open( "", caseData.p() );
caseData->mainGrid()->computeCachedData();
eclipseCase->setReservoirData( caseData.p() );
const size_t cellCount = caseData->mainGrid()->totalCellCount();
RigCaseCellResultsData* cellResults = caseData->results( RiaDefines::PorosityModelType::MATRIX_MODEL );
addStaticResult( cellResults, "DX", cellCount, 50.0 );
addStaticResult( cellResults, "DY", cellCount, 50.0 );
addStaticResult( cellResults, "DZ", cellCount, 50.0 );
addStaticResult( cellResults, "PERMX", cellCount, 100.0 );
addStaticResult( cellResults, "PERMY", cellCount, 100.0 );
addStaticResult( cellResults, "PERMZ", cellCount, 10.0 );
addStaticResult( cellResults, "NTG", cellCount, 1.0 );
addStaticResult( cellResults, "PORO", cellCount, 0.2 );
return eclipseCase;
}
// Vertical well through the two upper-left cells, perforated over the full grid height
class PerforatedWellFixture
{
public:
explicit PerforatedWellFixture( RiaDefines::EclipseUnitSystem unitSystem )
: unitSystem( unitSystem )
, wellRadius( unitSystem == RiaDefines::EclipseUnitSystem::UNITS_FIELD ? 0.328 : 0.1 )
, eclipseCase( createMockEclipseCase( unitSystem ) )
, project( new RimProject )
, wellPath( new RimWellPath )
{
wellPath->setName( "TestWell" );
wellPath->setUnitSystem( unitSystem );
cvf::ref<RigWellPath> geometry = new RigWellPath( { cvf::Vec3d( 25, 25, 10 ), cvf::Vec3d( 25, 25, -110 ) }, { 0.0, 120.0 } );
wellPath->setWellPathGeometry( geometry.p() );
project->oilFields[0]->wellPathCollection->addWellPath( wellPath );
auto* interval = new RimPerforationInterval;
interval->setStartAndEndMD( 10.0, 110.0 );
interval->setDiameter( 2.0 * wellRadius );
interval->setSkinFactor( 0.0 );
wellPath->perforationIntervalCollection()->appendPerforation( interval );
}
~PerforatedWellFixture()
{
delete eclipseCase;
project->close();
delete project;
}
void setNonDarcyType( RimNonDarcyPerforationParameters::NonDarcyFlowEnum type )
{
auto* params = const_cast<RimNonDarcyPerforationParameters*>( wellPath->perforationIntervalCollection()->nonDarcyParameters() );
auto* field = dynamic_cast<caf::PdmField<caf::AppEnum<RimNonDarcyPerforationParameters::NonDarcyFlowEnum>>*>(
params->findField( "NonDarcyFlowType" ) );
ASSERT_NE( nullptr, field );
field->setValue( type );
}
void setUserDefinedDFactor( double dFactor )
{
auto* params = const_cast<RimNonDarcyPerforationParameters*>( wellPath->perforationIntervalCollection()->nonDarcyParameters() );
auto* field = dynamic_cast<caf::PdmField<double>*>( params->findField( "UserDefinedDFactor" ) );
ASSERT_NE( nullptr, field );
field->setValue( dFactor );
}
std::vector<RigCompletionData> completions()
{
return RicWellPathExportCompletionDataFeatureImpl::completionDataForWellPath( wellPath, eclipseCase );
}
RiaDefines::EclipseUnitSystem unitSystem;
double wellRadius;
RimEclipseResultCase* eclipseCase;
RimProject* project;
RimWellPath* wellPath;
};
RigCompletionData makeCompletion( double startMD, double endMD, std::optional<int> completionNumber )
{
RigCompletionData completion( "W", RigCompletionDataGridCell( 0, nullptr ), startMD );
@@ -49,7 +179,7 @@ std::optional<int> numberForAllOrders( std::vector<RigCompletionData> completion
for ( size_t i : order )
permuted.push_back( completions[i] );
auto number = RicWellPathExportCompletionDataFeatureImpl::completionNumberForCell( permuted );
auto number = RicCompletionDataExportTools::completionNumberForCell( permuted );
if ( !firstSet )
{
first = number;
@@ -104,5 +234,134 @@ TEST( RicWellPathExportCompletionDataFeatureImpl, CompletionNumberIgnoresContrib
//--------------------------------------------------------------------------------------------------
TEST( RicWellPathExportCompletionDataFeatureImpl, CompletionNumberEmptyCompletionsGivesNoNumber )
{
EXPECT_EQ( std::nullopt, RicWellPathExportCompletionDataFeatureImpl::completionNumberForCell( {} ) );
EXPECT_EQ( std::nullopt, RicCompletionDataExportTools::completionNumberForCell( {} ) );
}
class CompdatNonDarcyTest : public ::testing::TestWithParam<RiaDefines::EclipseUnitSystem>
{
};
//--------------------------------------------------------------------------------------------------
/// Without non-Darcy flow, COMPDAT connections get a transmissibility but the D-factor is defaulted
//--------------------------------------------------------------------------------------------------
TEST_P( CompdatNonDarcyTest, WithoutNonDarcyHasDefaultDFactor )
{
PerforatedWellFixture fixture( GetParam() );
fixture.setNonDarcyType( RimNonDarcyPerforationParameters::NON_DARCY_NONE );
auto completions = fixture.completions();
ASSERT_EQ( 2u, completions.size() );
for ( const auto& completion : completions )
{
EXPECT_GT( completion.transmissibility(), 0.0 );
EXPECT_TRUE( RigCompletionData::isDefaultValue( completion.dFactor() ) );
}
}
//--------------------------------------------------------------------------------------------------
/// User-defined well D-factor is distributed as D_conn = D_well * sum(T) / T_conn and exported negative
//--------------------------------------------------------------------------------------------------
TEST_P( CompdatNonDarcyTest, WithUserDefinedDFactor )
{
PerforatedWellFixture fixture( GetParam() );
fixture.setNonDarcyType( RimNonDarcyPerforationParameters::NON_DARCY_USER_DEFINED );
fixture.setUserDefinedDFactor( 0.5 );
auto completions = fixture.completions();
ASSERT_EQ( 2u, completions.size() );
double sumT = 0.0;
for ( const auto& completion : completions )
sumT += completion.transmissibility();
for ( const auto& completion : completions )
{
EXPECT_LT( completion.dFactor(), 0.0 );
EXPECT_NEAR( -0.5 * sumT / completion.transmissibility(), completion.dFactor(), 1.0e-9 );
}
// Identical cells share the well D-factor equally
EXPECT_NEAR( completions[0].dFactor(), completions[1].dFactor(), 1.0e-9 );
EXPECT_NEAR( -1.0, completions[0].dFactor(), 1.0e-9 );
}
//--------------------------------------------------------------------------------------------------
/// Computed D-factor must match the D-factor equation applied to the connection's effective h and K
//--------------------------------------------------------------------------------------------------
TEST_P( CompdatNonDarcyTest, WithComputedDFactor )
{
PerforatedWellFixture fixture( GetParam() );
fixture.setNonDarcyType( RimNonDarcyPerforationParameters::NON_DARCY_COMPUTED );
auto completions = fixture.completions();
ASSERT_EQ( 2u, completions.size() );
const auto* params = fixture.wellPath->perforationIntervalCollection()->nonDarcyParameters();
const double alpha = RiaDefines::nonDarcyFlowAlpha( GetParam() );
for ( const auto& completion : completions )
{
// Vertical well: 50 length units of well inside each cell, along K
auto data = RicTransmissibilityCalculator::calculateTransmissibilityData( fixture.eclipseCase,
fixture.wellPath,
cvf::Vec3d( 0.0, 0.0, 50.0 ),
0.0,
fixture.wellRadius,
completion.completionDataGridCell().globalCellIndex(),
false );
ASSERT_TRUE( data.isValid() );
const double effectiveK = params->gridPermeabilityScalingFactor() * data.effectiveK();
const double beta = RigPerforationTransmissibilityEquations::betaFactor( params->inertialCoefficientBeta0(),
effectiveK,
params->permeabilityScalingFactor(),
0.2,
params->porosityScalingFactor() );
// The radius parameter is entered in meters; the field alpha expects feet
const double radiusForDFactor = ( GetParam() == RiaDefines::EclipseUnitSystem::UNITS_FIELD )
? params->wellRadius() * RiaEclipseUnitTools::feetPerMeter()
: params->wellRadius();
const double expected = RigPerforationTransmissibilityEquations::dFactor( alpha,
beta,
effectiveK,
data.effectiveH(),
radiusForDFactor,
params->relativeGasDensity(),
params->gasViscosity() );
ASSERT_TRUE( std::isfinite( expected ) );
EXPECT_GT( expected, 0.0 );
EXPECT_FALSE( RigCompletionData::isDefaultValue( completion.dFactor() ) );
EXPECT_NEAR( -expected, completion.dFactor(), 1.0e-9 * expected );
}
}
//--------------------------------------------------------------------------------------------------
/// The connection factor itself must not depend on the non-Darcy setting
//--------------------------------------------------------------------------------------------------
TEST_P( CompdatNonDarcyTest, TransmissibilityIndependentOfNonDarcy )
{
PerforatedWellFixture fixture( GetParam() );
fixture.setNonDarcyType( RimNonDarcyPerforationParameters::NON_DARCY_NONE );
auto without = fixture.completions();
fixture.setNonDarcyType( RimNonDarcyPerforationParameters::NON_DARCY_COMPUTED );
auto with = fixture.completions();
ASSERT_EQ( without.size(), with.size() );
for ( size_t i = 0; i < with.size(); ++i )
{
EXPECT_DOUBLE_EQ( without[i].transmissibility(), with[i].transmissibility() );
}
}
INSTANTIATE_TEST_SUITE_P( MetricAndField,
CompdatNonDarcyTest,
::testing::Values( RiaDefines::EclipseUnitSystem::UNITS_METRIC, RiaDefines::EclipseUnitSystem::UNITS_FIELD ),
[]( const ::testing::TestParamInfo<RiaDefines::EclipseUnitSystem>& info )
{ return info.param == RiaDefines::EclipseUnitSystem::UNITS_FIELD ? "Field" : "Metric"; } );
@@ -0,0 +1,70 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2025 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 "RigPerforationTransmissibilityEquations.h"
#include <cmath>
TEST( RigPerforationTransmissibilityEquations, BetaFactor )
{
// beta = inertialCoefficient * Ke^permScaling * phi^porosityScaling
double beta = RigPerforationTransmissibilityEquations::betaFactor( 2.0, 100.0, 0.5, 0.25, 2.0 );
EXPECT_NEAR( 2.0 * 10.0 * 0.0625, beta, 1.0e-12 );
// Zero exponents leave only the inertial coefficient
EXPECT_DOUBLE_EQ( 3.5, RigPerforationTransmissibilityEquations::betaFactor( 3.5, 123.0, 0.0, 0.3, 0.0 ) );
}
TEST( RigPerforationTransmissibilityEquations, BetaFactorNegativeExponents )
{
// Typical non-Darcy correlation uses negative exponents: beta = C * Ke^-1.2 * phi^-0.5
double beta = RigPerforationTransmissibilityEquations::betaFactor( 1.0e6, 100.0, -1.2, 0.25, -0.5 );
EXPECT_NEAR( 1.0e6 * std::pow( 100.0, -1.2 ) * 2.0, beta, 1.0e-6 );
// Unit permeability and porosity make the exponents irrelevant
EXPECT_DOUBLE_EQ( 7.0, RigPerforationTransmissibilityEquations::betaFactor( 7.0, 1.0, -1.2, 1.0, -0.5 ) );
// Linear in the inertial coefficient
const double base = RigPerforationTransmissibilityEquations::betaFactor( 1.0, 50.0, -1.2, 0.2, -0.5 );
EXPECT_NEAR( 3.0 * base, RigPerforationTransmissibilityEquations::betaFactor( 3.0, 50.0, -1.2, 0.2, -0.5 ), 1.0e-12 );
}
TEST( RigPerforationTransmissibilityEquations, DFactor )
{
// D = alpha * beta * Ke / h * 1 / rw * rho / mu
double d = RigPerforationTransmissibilityEquations::dFactor( 2.0, 3.0, 40.0, 5.0, 0.1, 0.8, 0.02 );
EXPECT_NEAR( 2.0 * 3.0 * 8.0 * 10.0 * 40.0, d, 1.0e-9 );
}
TEST( RigPerforationTransmissibilityEquations, DFactorScaling )
{
const double base = RigPerforationTransmissibilityEquations::dFactor( 1.0, 1.0, 10.0, 2.0, 0.1, 1.0, 1.0 );
// Linear in unit constant, beta, permeability, density
EXPECT_NEAR( 2.0 * base, RigPerforationTransmissibilityEquations::dFactor( 2.0, 1.0, 10.0, 2.0, 0.1, 1.0, 1.0 ), 1.0e-9 );
EXPECT_NEAR( 2.0 * base, RigPerforationTransmissibilityEquations::dFactor( 1.0, 2.0, 10.0, 2.0, 0.1, 1.0, 1.0 ), 1.0e-9 );
EXPECT_NEAR( 2.0 * base, RigPerforationTransmissibilityEquations::dFactor( 1.0, 1.0, 20.0, 2.0, 0.1, 1.0, 1.0 ), 1.0e-9 );
EXPECT_NEAR( 2.0 * base, RigPerforationTransmissibilityEquations::dFactor( 1.0, 1.0, 10.0, 2.0, 0.1, 2.0, 1.0 ), 1.0e-9 );
// Inversely proportional to length, well radius and viscosity
EXPECT_NEAR( 0.5 * base, RigPerforationTransmissibilityEquations::dFactor( 1.0, 1.0, 10.0, 4.0, 0.1, 1.0, 1.0 ), 1.0e-9 );
EXPECT_NEAR( 0.5 * base, RigPerforationTransmissibilityEquations::dFactor( 1.0, 1.0, 10.0, 2.0, 0.2, 1.0, 1.0 ), 1.0e-9 );
EXPECT_NEAR( 0.5 * base, RigPerforationTransmissibilityEquations::dFactor( 1.0, 1.0, 10.0, 2.0, 0.1, 1.0, 2.0 ), 1.0e-9 );
}
@@ -0,0 +1,205 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 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 "RiaDefines.h"
#include "RiaFractureDefines.h"
#include "RimEllipseFractureTemplate.h"
#include <cmath>
#include <limits>
//--------------------------------------------------------------------------------------------------
/// Exposes protected settings of the template, so that the values reported by
/// wellFractureIntersectionData() can be verified through the public compute functions
//--------------------------------------------------------------------------------------------------
class TestEllipseFractureTemplate : public RimEllipseFractureTemplate
{
public:
explicit TestEllipseFractureTemplate( RiaDefines::EclipseUnitSystem unit )
{
setUnitSystem( unit );
setDefaultValuesFromUnit();
}
void useFractureData()
{
m_fractureWidthType = WIDTH_FROM_FRACTURE;
m_permeabilityType = CONDUCTIVITY_FROM_FRACTURE;
m_betaFactorType = USER_DEFINED_BETA_FACTOR;
m_relativePermeability = 1.0;
}
void useUserDefinedWidth( double width )
{
m_fractureWidthType = USER_DEFINED_WIDTH;
m_fractureWidth = width;
}
void useComputedDFactor( double inertialCoefficient )
{
m_nonDarcyFlowType = NON_DARCY_COMPUTED;
m_inertialCoefficient = inertialCoefficient;
}
void useUserDefinedDFactor( double dFactor )
{
m_nonDarcyFlowType = NON_DARCY_USER_DEFINED;
m_userDefinedDFactor = dFactor;
}
void setRelativePermeability( double value ) { m_relativePermeability = value; }
void setOrientation( FracOrientationEnum orientation ) { m_orientationType = orientation; }
double wellDiameterValue() const { return m_wellDiameter; }
double gasDensity() const { return m_relativeGasDensity; }
double gasViscosityValue() const { return m_gasViscosity; }
};
//--------------------------------------------------------------------------------------------------
/// Ellipse templates report the user entered width and permeability as intersection data
//--------------------------------------------------------------------------------------------------
TEST( RimFractureTemplateNonDarcy, EllipseIntersectionDataMetric )
{
TestEllipseFractureTemplate tmpl( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
tmpl.useFractureData();
// Metric defaults: width 0.01 m, permeability 100000 mD
EXPECT_DOUBLE_EQ( 0.01, tmpl.computeFractureWidth( nullptr ) );
EXPECT_DOUBLE_EQ( 100000.0, tmpl.computeEffectivePermeability( nullptr ) );
EXPECT_NEAR( 1000.0, tmpl.computeKh( nullptr ), 1.0e-9 );
}
TEST( RimFractureTemplateNonDarcy, EllipseIntersectionDataField )
{
TestEllipseFractureTemplate tmpl( RiaDefines::EclipseUnitSystem::UNITS_FIELD );
tmpl.useFractureData();
// Field defaults: width 0.5 inches (reported in feet), permeability 80000 mD
EXPECT_NEAR( 0.5 / 12.0, tmpl.computeFractureWidth( nullptr ), 1.0e-12 );
EXPECT_DOUBLE_EQ( 80000.0, tmpl.computeEffectivePermeability( nullptr ) );
EXPECT_NEAR( 80000.0 * 0.5 / 12.0, tmpl.computeKh( nullptr ), 1.0e-9 );
}
TEST( RimFractureTemplateNonDarcy, RelativePermeabilityScalesEffectivePermeability )
{
TestEllipseFractureTemplate tmpl( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
tmpl.useFractureData();
tmpl.setRelativePermeability( 0.25 );
EXPECT_DOUBLE_EQ( 25000.0, tmpl.computeEffectivePermeability( nullptr ) );
EXPECT_NEAR( 250.0, tmpl.computeKh( nullptr ), 1.0e-9 );
}
TEST( RimFractureTemplateNonDarcy, UserDefinedWidthOverridesIntersectionWidth )
{
TestEllipseFractureTemplate tmpl( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
tmpl.useFractureData();
tmpl.useUserDefinedWidth( 0.05 );
EXPECT_DOUBLE_EQ( 0.05, tmpl.computeFractureWidth( nullptr ) );
EXPECT_NEAR( 100000.0 * 0.05, tmpl.computeKh( nullptr ), 1.0e-9 );
}
//--------------------------------------------------------------------------------------------------
/// D = alpha * beta * Ke / h * 1 / rw * gamma / mu, using the intersection data of the template
//--------------------------------------------------------------------------------------------------
TEST( RimFractureTemplateNonDarcy, ComputedDFactorMatchesEquation )
{
TestEllipseFractureTemplate tmpl( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
tmpl.useFractureData();
tmpl.useComputedDFactor( 0.5 );
const double alpha = RiaDefines::nonDarcyFlowAlpha( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
const double radius = tmpl.wellDiameterValue() / 2.0;
const double ke = 100000.0;
const double h = 0.01;
const double expected = alpha * 0.5 * ke * tmpl.gasDensity() / ( h * radius * tmpl.gasViscosityValue() );
EXPECT_NEAR( expected, tmpl.computeDFactor( nullptr ), expected * 1.0e-12 );
EXPECT_NEAR( radius, tmpl.computeWellRadiusForDFactorCalculation( nullptr ), 1.0e-12 );
}
TEST( RimFractureTemplateNonDarcy, ComputedDFactorUsesUnitDependentAlpha )
{
TestEllipseFractureTemplate metric( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
metric.useFractureData();
metric.useComputedDFactor( 1.0 );
metric.useUserDefinedWidth( 0.01 );
TestEllipseFractureTemplate field( RiaDefines::EclipseUnitSystem::UNITS_FIELD );
field.useFractureData();
field.useComputedDFactor( 1.0 );
field.useUserDefinedWidth( 0.01 );
// Make everything but alpha, permeability and well radius equal between the two templates
const double ratio = ( field.computeDFactor( nullptr ) / metric.computeDFactor( nullptr ) );
// The field well diameter is given in inches, but the radius used in the D-factor must be in feet
EXPECT_NEAR( field.wellDiameterValue() / 2.0 / 12.0, field.computeWellRadiusForDFactorCalculation( nullptr ), 1.0e-12 );
const double alphaRatio = RiaDefines::nonDarcyFlowAlpha( RiaDefines::EclipseUnitSystem::UNITS_FIELD ) /
RiaDefines::nonDarcyFlowAlpha( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
const double permRatio = field.computeEffectivePermeability( nullptr ) / metric.computeEffectivePermeability( nullptr );
const double metricRadius = metric.wellDiameterValue() / 2.0; // [m]
const double fieldRadius = field.wellDiameterValue() / 2.0 / 12.0; // inches -> [ft]
const double radiusRatio = metricRadius / fieldRadius;
EXPECT_NEAR( alphaRatio * permRatio * radiusRatio, ratio, ratio * 1.0e-12 );
}
TEST( RimFractureTemplateNonDarcy, DFactorScaleFactorAndAlongWellPathCorrection )
{
TestEllipseFractureTemplate tmpl( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
tmpl.useFractureData();
tmpl.useComputedDFactor( 0.5 );
const double base = tmpl.computeDFactor( nullptr );
tmpl.setScaleFactors( 1.0, 1.0, 3.0, 1.0 );
EXPECT_NEAR( 3.0 * base, tmpl.computeDFactor( nullptr ), base * 1.0e-12 );
// Linear inflow correction factor of 1.2 for fractures along the well path
tmpl.setScaleFactors( 1.0, 1.0, 1.0, 1.0 );
tmpl.setOrientation( RimFractureTemplate::ALONG_WELL_PATH );
EXPECT_NEAR( 1.2 * base, tmpl.computeDFactor( nullptr ), base * 1.0e-12 );
}
TEST( RimFractureTemplateNonDarcy, UserDefinedDFactorIsScaledButNotComputed )
{
TestEllipseFractureTemplate tmpl( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
tmpl.useFractureData();
tmpl.useUserDefinedDFactor( 0.125 );
EXPECT_DOUBLE_EQ( 0.125, tmpl.computeDFactor( nullptr ) );
tmpl.setScaleFactors( 1.0, 1.0, 2.0, 1.0 );
EXPECT_DOUBLE_EQ( 0.25, tmpl.computeDFactor( nullptr ) );
}
TEST( RimFractureTemplateNonDarcy, ZeroWidthGivesInfiniteDFactor )
{
TestEllipseFractureTemplate tmpl( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
tmpl.useFractureData();
tmpl.useComputedDFactor( 0.5 );
tmpl.useUserDefinedWidth( 0.0 );
EXPECT_EQ( std::numeric_limits<double>::infinity(), tmpl.computeDFactor( nullptr ) );
}
@@ -0,0 +1,151 @@
/////////////////////////////////////////////////////////////////////////////////
//
// Copyright (C) 2026 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 "RiaTestDataDirectory.h"
#include "Well/RigWellPath.h"
#include "RimStimPlanFractureTemplate.h"
#include "RimWellPath.h"
#include "RimWellPathFracture.h"
#include "RimWellPathFractureCollection.h"
#include <cmath>
static const QString STIMPLAN_TEMPLATE_TEST_DATA_DIRECTORY = QString( "%1/RimStimPlanFractureTemplate/" ).arg( TEST_DATA_DIR );
static const QString STIMPLAN_XML_READER_TEST_DATA_DIRECTORY = QString( "%1/RifStimPlanXmlReader/" ).arg( TEST_DATA_DIR );
//--------------------------------------------------------------------------------------------------
/// Gives access to the protected settings needed to load a template from file
//--------------------------------------------------------------------------------------------------
class TestStimPlanFractureTemplate : public RimStimPlanFractureTemplate
{
public:
explicit TestStimPlanFractureTemplate( const QString& fileName )
{
setFileName( fileName );
loadDataAndUpdate();
m_fractureWidthType = WIDTH_FROM_FRACTURE;
m_permeabilityType = CONDUCTIVITY_FROM_FRACTURE;
}
void useBetaFromFile() { m_betaFactorType = BETA_FACTOR_FROM_FRACTURE; }
void setPerforationLengthValue( double length ) { m_perforationLength = length; }
void setOrientation( FracOrientationEnum orientation ) { m_orientationType = orientation; }
};
//--------------------------------------------------------------------------------------------------
/// Width, conductivity and beta at the well position are taken from the fracture grid, and the
/// beta factor is converted from file units to Forchheimer units
//--------------------------------------------------------------------------------------------------
TEST( RimStimPlanFractureTemplateTest, IntersectionDataAtWellCenterFromFile )
{
TestStimPlanFractureTemplate tmpl( STIMPLAN_TEMPLATE_TEST_DATA_DIRECTORY + "uniform_width_beta.xml" );
tmpl.useBetaFromFile();
RimWellPathFracture fracture;
fracture.setFractureTemplate( &tmpl );
ASSERT_TRUE( fracture.fractureGrid() != nullptr );
// Width 0.01 m and conductivity 300 md-m gives permeability 30000 mD
EXPECT_NEAR( 0.01, tmpl.computeFractureWidth( &fracture ), 1.0e-12 );
EXPECT_NEAR( 30000.0, tmpl.computeEffectivePermeability( &fracture ), 1.0e-6 );
EXPECT_NEAR( 300.0, tmpl.computeKh( &fracture ), 1.0e-9 );
// 2.0265e8 [/m] divided by 1.01325e8 gives 2.0 Forchheimer units
EXPECT_NEAR( 2.0, tmpl.getOrComputeBetaFactor( &fracture ), 1.0e-9 );
}
TEST( RimStimPlanFractureTemplateTest, BetaFactorIsUserDefinedByDefault )
{
TestStimPlanFractureTemplate tmpl( STIMPLAN_TEMPLATE_TEST_DATA_DIRECTORY + "uniform_width_beta.xml" );
RimWellPathFracture fracture;
fracture.setFractureTemplate( &tmpl );
EXPECT_NEAR( 0.006083236, tmpl.getOrComputeBetaFactor( &fracture ), 1.0e-12 );
}
//--------------------------------------------------------------------------------------------------
/// Without a fracture instance there is no intersection data: width is zero and beta is undefined
//--------------------------------------------------------------------------------------------------
TEST( RimStimPlanFractureTemplateTest, NoFractureInstanceGivesNoIntersectionData )
{
TestStimPlanFractureTemplate tmpl( STIMPLAN_TEMPLATE_TEST_DATA_DIRECTORY + "uniform_width_beta.xml" );
tmpl.useBetaFromFile();
EXPECT_DOUBLE_EQ( 0.0, tmpl.computeFractureWidth( nullptr ) );
EXPECT_TRUE( std::isinf( tmpl.getOrComputeBetaFactor( nullptr ) ) );
}
//--------------------------------------------------------------------------------------------------
/// A file without width values has no width data, and the conductivity at the well is used directly
//--------------------------------------------------------------------------------------------------
TEST( RimStimPlanFractureTemplateTest, FileWithoutWidthUsesConductivityDirectly )
{
TestStimPlanFractureTemplate tmpl( STIMPLAN_XML_READER_TEST_DATA_DIRECTORY + "small_fracture.xml" );
RimWellPathFracture fracture;
fracture.setFractureTemplate( &tmpl );
ASSERT_TRUE( fracture.fractureGrid() != nullptr );
// The conductivity at the well center is 301.7 md-m in the file
EXPECT_NEAR( 301.7, tmpl.computeKh( &fracture ), 1.0e-9 );
}
//--------------------------------------------------------------------------------------------------
/// For fractures along the well path, the values are averaged along the part of the well path
/// that intersects the fracture
//--------------------------------------------------------------------------------------------------
TEST( RimStimPlanFractureTemplateTest, IntersectionDataAlongWellPath )
{
// Horizontal well along the X axis at depth 2015 m, with measured depth equal to the X coordinate + 500
std::vector<cvf::Vec3d> wellPathPoints;
std::vector<double> measuredDepths;
for ( int i = 0; i <= 20; i++ )
{
const double x = -500.0 + i * 50.0;
wellPathPoints.push_back( cvf::Vec3d( x, 0.0, -2015.0 ) );
measuredDepths.push_back( x + 500.0 );
}
RimWellPath wellPath;
wellPath.setWellPathGeometry( new RigWellPath( wellPathPoints, measuredDepths ) );
TestStimPlanFractureTemplate tmpl( STIMPLAN_TEMPLATE_TEST_DATA_DIRECTORY + "uniform_width_beta.xml" );
tmpl.useBetaFromFile();
tmpl.setOrientation( RimFractureTemplate::ALONG_WELL_PATH );
tmpl.setPerforationLengthValue( 80.0 );
auto fracture = new RimWellPathFracture;
wellPath.fractureCollection()->addFracture( fracture );
fracture->setFractureUnit( RiaDefines::EclipseUnitSystem::UNITS_METRIC );
fracture->setFractureTemplate( &tmpl );
fracture->setMeasuredDepth( 500.0 );
ASSERT_TRUE( fracture->fractureGrid() != nullptr );
EXPECT_NEAR( 0.01, tmpl.computeFractureWidth( fracture ), 1.0e-9 );
EXPECT_NEAR( 30000.0, tmpl.computeEffectivePermeability( fracture ), 1.0e-3 );
EXPECT_NEAR( 300.0, tmpl.computeKh( fracture ), 1.0e-6 );
EXPECT_NEAR( 2.0, tmpl.getOrComputeBetaFactor( fracture ), 1.0e-9 );
}
@@ -0,0 +1,52 @@
<contours>
<grid xCount="4" yCount="5" uom="m" cs="tvd" frac="1">
<xs><![CDATA[0
50 100 150
]]></xs>
<ys><![CDATA[2035 2025 2015 2005 1995 ]]></ys>
</grid>
<properties>
<property name="CONDUCTIVITY" uom="md-m">
<time value="117.238">
<depth>1995.00</depth>
<![CDATA[300 300 300 300]]>
<depth>2005.00</depth>
<![CDATA[300 300 300 300]]>
<depth>2015.00</depth>
<![CDATA[300 300 300 300]]>
<depth>2025.00</depth>
<![CDATA[300 300 300 300]]>
<depth>2035.00</depth>
<![CDATA[300 300 300 300]]>
</time>
</property>
<property name="Effective Width" uom="m">
<time value="117.238">
<depth>1995.00</depth>
<![CDATA[0.01 0.01 0.01 0.01]]>
<depth>2005.00</depth>
<![CDATA[0.01 0.01 0.01 0.01]]>
<depth>2015.00</depth>
<![CDATA[0.01 0.01 0.01 0.01]]>
<depth>2025.00</depth>
<![CDATA[0.01 0.01 0.01 0.01]]>
<depth>2035.00</depth>
<![CDATA[0.01 0.01 0.01 0.01]]>
</time>
</property>
<property name="Beta" uom="/m">
<time value="117.238">
<depth>1995.00</depth>
<![CDATA[2.0265e8 2.0265e8 2.0265e8 2.0265e8]]>
<depth>2005.00</depth>
<![CDATA[2.0265e8 2.0265e8 2.0265e8 2.0265e8]]>
<depth>2015.00</depth>
<![CDATA[2.0265e8 2.0265e8 2.0265e8 2.0265e8]]>
<depth>2025.00</depth>
<![CDATA[2.0265e8 2.0265e8 2.0265e8 2.0265e8]]>
<depth>2035.00</depth>
<![CDATA[2.0265e8 2.0265e8 2.0265e8 2.0265e8]]>
</time>
</property>
</properties>
</contours>